/**
  * {@inheritDoc}
  */
 public function findMulti(array $queries, array $options = array())
 {
     if (!$queries) {
         return array();
     }
     // get cache keys for all queries
     $cacheKeys = $this->getCacheKeys($queries);
     // retrieve from cache (only query duplicate queries once)
     // $fromCache will be an array containing compacted results as value and
     // cache keys as key
     $fromCache = $this->cache->getMulti(array_unique($cacheKeys));
     // figure out what queries were resolved in cache
     // $keysFromCache will be an array where values are cache keys and keys
     // are the same index as their corresponding $queries
     $keysFromCache = array_intersect($cacheKeys, array_keys($fromCache));
     // filter out all queries that have been resolved from cache and fetch
     // them from storage
     // $fromStorage will be an array containing (expanded) results as value
     // and indexes matching $query as key
     $storageQueries = array_diff_key($queries, $keysFromCache);
     $fromStorage = array();
     if ($storageQueries) {
         $fromStorage = $this->backingStoreFindMulti($storageQueries);
         // store the data we've just retrieved to cache
         foreach ($fromStorage as $index => $rows) {
             // backing store returns data that may not be valid to cache (e.g.
             // if we couldn't retrieve content from ExternalStore, we shouldn't
             // cache that result)
             $rows = array_filter($rows, array($this->storage, 'validate'));
             if ($rows !== $fromStorage[$index]) {
                 continue;
             }
             $compacted = $this->rowCompactor->compactRows($rows);
             $callback = function (\BagOStuff $cache, $key, $value) use($compacted) {
                 if ($value !== false) {
                     // somehow, the data was already cached in the meantime
                     return false;
                 }
                 return $compacted;
             };
             $this->cache->merge($cacheKeys[$index], $callback);
         }
     }
     $results = $fromStorage;
     // $queries may have had duplicates that we've ignored to minimize
     // cache requests - now re-duplicate values from cache & match the
     // results against their respective original keys in $queries
     foreach ($keysFromCache as $index => $cacheKey) {
         $results[$index] = $fromCache[$cacheKey];
     }
     // now that we have all data, both from cache & backing storage, filter
     // out all data we don't need
     $results = $this->filterResults($results, $options);
     // if we have no data from cache, there's nothing left - quit early
     if (!$fromCache) {
         return $results;
     }
     // because we may have combined data from 2 different sources, chances
     // are the order of the data is no longer in sync with the order
     // $queries were in - fix that by replacing $queries values with
     // the corresponding $results value
     // note that there may be missing results, hence the intersect ;)
     $order = array_intersect_key($queries, $results);
     $results = array_replace($order, $results);
     $keyToQuery = array();
     foreach ($keysFromCache as $index => $key) {
         // all redundant data has been stripped, now expand all cache values
         // (we're only doing this now to avoid expanding redundant data)
         $fromCache[$key] = $results[$index];
         // to expand rows, we'll need the $query info mapped to the cache
         // key instead of the $query index
         if (!isset($keyToQuery[$key])) {
             $keyToQuery[$key] = $queries[$index];
             $keyToQuery[$key] = UUID::convertUUIDs($keyToQuery[$key], 'alphadecimal');
         }
     }
     // expand and replace the stubs in $results with complete data
     $fromCache = $this->rowCompactor->expandCacheResult($fromCache, $keyToQuery);
     foreach ($keysFromCache as $index => $cacheKey) {
         $results[$index] = $fromCache[$cacheKey];
     }
     return $results;
 }