"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Framework/Flex/FlexIndex.php" (1 Sep 2020, 20227 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "FlexIndex.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Framework\Flex
    5  *
    6  * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
    7  * @license    MIT License; see LICENSE file for details.
    8  */
    9 
   10 namespace Grav\Framework\Flex;
   11 
   12 use Grav\Common\Debugger;
   13 use Grav\Common\File\CompiledYamlFile;
   14 use Grav\Common\Grav;
   15 use Grav\Common\Session;
   16 use Grav\Framework\Cache\CacheInterface;
   17 use Grav\Framework\Collection\CollectionInterface;
   18 use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
   19 use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
   20 use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
   21 use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
   22 use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
   23 use Grav\Framework\Object\Interfaces\ObjectInterface;
   24 use Grav\Framework\Object\ObjectIndex;
   25 use Monolog\Logger;
   26 use Psr\SimpleCache\InvalidArgumentException;
   27 
   28 class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface
   29 {
   30     /** @var FlexDirectory */
   31     private $_flexDirectory;
   32 
   33     /** @var string */
   34     private $_keyField;
   35 
   36     /** @var array */
   37     private $_indexKeys;
   38 
   39     /**
   40      * @param FlexDirectory $directory
   41      * @return static
   42      */
   43     public static function createFromStorage(FlexDirectory $directory)
   44     {
   45         return static::createFromArray(static::loadEntriesFromStorage($directory->getStorage()), $directory);
   46     }
   47 
   48     /**
   49      * {@inheritdoc}
   50      * @see FlexCollectionInterface::createFromArray()
   51      */
   52     public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
   53     {
   54         $instance = new static($entries, $directory);
   55         $instance->setKeyField($keyField);
   56 
   57         return $instance;
   58     }
   59 
   60     /**
   61      * @param FlexStorageInterface $storage
   62      * @return array
   63      */
   64     public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
   65     {
   66         return $storage->getExistingKeys();
   67     }
   68 
   69     /**
   70      * Initializes a new FlexIndex.
   71      *
   72      * @param array $entries
   73      * @param FlexDirectory|null $directory
   74      */
   75     public function __construct(array $entries = [], FlexDirectory $directory = null)
   76     {
   77         parent::__construct($entries);
   78 
   79         $this->_flexDirectory = $directory;
   80         $this->setKeyField(null);
   81     }
   82 
   83     /**
   84      * {@inheritdoc}
   85      * @see FlexCollectionInterface::search()
   86      */
   87     public function search(string $search, $properties = null, array $options = null)
   88     {
   89         return $this->__call('search', [$search, $properties, $options]);
   90     }
   91 
   92     /**
   93      * {@inheritdoc}
   94      * @see FlexCollectionInterface::sort()
   95      */
   96     public function sort(array $orderings)
   97     {
   98         return $this->orderBy($orderings);
   99     }
  100 
  101 
  102     /**
  103      * {@inheritdoc}
  104      * @see FlexCollectionInterface::filterBy()
  105      */
  106     public function filterBy(array $filters)
  107     {
  108         return $this->__call('filterBy', [$filters]);
  109     }
  110 
  111     /**
  112      * {@inheritdoc}
  113      * @see FlexCollectionInterface::getFlexType()
  114      */
  115     public function getFlexType(): string
  116     {
  117         return $this->_flexDirectory->getFlexType();
  118     }
  119 
  120     /**
  121      * {@inheritdoc}
  122      * @see FlexCollectionInterface::getFlexDirectory()
  123      */
  124     public function getFlexDirectory(): FlexDirectory
  125     {
  126         return $this->_flexDirectory;
  127     }
  128 
  129     /**
  130      * {@inheritdoc}
  131      * @see FlexCollectionInterface::getTimestamp()
  132      */
  133     public function getTimestamp(): int
  134     {
  135         $timestamps = $this->getTimestamps();
  136 
  137         return $timestamps ? max($timestamps) : time();
  138     }
  139 
  140     /**
  141      * {@inheritdoc}
  142      * @see FlexCollectionInterface::getCacheKey()
  143      */
  144     public function getCacheKey(): string
  145     {
  146         return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField);
  147     }
  148 
  149     /**
  150      * {@inheritdoc}
  151      * @see FlexCollectionInterface::getCacheChecksum()
  152      */
  153     public function getCacheChecksum(): string
  154     {
  155         return sha1($this->getCacheKey() . json_encode($this->getTimestamps()));
  156     }
  157 
  158     /**
  159      * {@inheritdoc}
  160      * @see FlexCollectionInterface::getTimestamps()
  161      */
  162     public function getTimestamps(): array
  163     {
  164         return $this->getIndexMap('storage_timestamp');
  165     }
  166 
  167     /**
  168      * {@inheritdoc}
  169      * @see FlexCollectionInterface::getStorageKeys()
  170      */
  171     public function getStorageKeys(): array
  172     {
  173         return $this->getIndexMap('storage_key');
  174     }
  175 
  176     /**
  177      * {@inheritdoc}
  178      * @see FlexCollectionInterface::getFlexKeys()
  179      */
  180     public function getFlexKeys(): array
  181     {
  182         // Get storage keys for the objects.
  183         $keys = [];
  184         $type = $this->_flexDirectory->getFlexType() . '.obj:';
  185 
  186         foreach ($this->getEntries() as $key => $value) {
  187             $keys[$key] = $value['flex_key'] ?? $type . $value['storage_key'];
  188         }
  189 
  190         return $keys;
  191     }
  192 
  193     /**
  194      * {@inheritdoc}
  195      * @see FlexIndexInterface::withKeyField()
  196      */
  197     public function withKeyField(string $keyField = null)
  198     {
  199         $keyField = $keyField ?: 'key';
  200         if ($keyField === $this->getKeyField()) {
  201             return $this;
  202         }
  203 
  204         $type = $keyField === 'flex_key' ? $this->_flexDirectory->getFlexType() . '.obj:' : '';
  205         $entries = [];
  206         foreach ($this->getEntries() as $key => $value) {
  207             if (!isset($value['key'])) {
  208                 $value['key'] = $key;
  209             }
  210 
  211             if (isset($value[$keyField])) {
  212                 $entries[$value[$keyField]] = $value;
  213             } elseif ($keyField === 'flex_key') {
  214                 $entries[$type . $value['storage_key']] = $value;
  215             }
  216         }
  217 
  218         return $this->createFrom($entries, $keyField);
  219     }
  220 
  221     /**
  222      * {@inheritdoc}
  223      * @see FlexCollectionInterface::getIndex()
  224      */
  225     public function getIndex()
  226     {
  227         return $this;
  228     }
  229 
  230     /**
  231      * {@inheritdoc}
  232      * @see FlexCollectionInterface::render()
  233      */
  234     public function render(string $layout = null, array $context = [])
  235     {
  236         return $this->__call('render', [$layout, $context]);
  237     }
  238 
  239     /**
  240      * @param bool $prefix
  241      * @return string
  242      * @deprecated 1.6 Use `->getFlexType()` instead.
  243      */
  244     public function getType($prefix = false)
  245     {
  246         user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  247 
  248         $type = $prefix ? $this->getTypePrefix() : '';
  249 
  250         return $type . $this->getFlexType();
  251     }
  252 
  253     /**
  254      * {@inheritdoc}
  255      * @see FlexIndexInterface::getFlexKeys()
  256      */
  257     public function getIndexMap(string $indexKey = null)
  258     {
  259         if (null === $indexKey) {
  260             return $this->getEntries();
  261         }
  262 
  263         // Get storage keys for the objects.
  264         $index = [];
  265         foreach ($this->getEntries() as $key => $value) {
  266             $index[$key] = $value[$indexKey] ?? null;
  267         }
  268 
  269         return $index;
  270     }
  271 
  272     /**
  273      * @return array
  274      */
  275     public function getMetaData(string $key): array
  276     {
  277         return $this->getEntries()[$key] ?? [];
  278     }
  279 
  280     /**
  281      * @return string
  282      */
  283     public function getKeyField() : string
  284     {
  285         return $this->_keyField ?? 'storage_key';
  286     }
  287 
  288     /**
  289      * @param string|null $namespace
  290      * @return CacheInterface
  291      */
  292     public function getCache(string $namespace = null)
  293     {
  294         return $this->_flexDirectory->getCache($namespace);
  295     }
  296 
  297     /**
  298      * @param array $orderings
  299      * @return FlexIndex|FlexCollection
  300      */
  301     public function orderBy(array $orderings)
  302     {
  303         if (!$orderings || !$this->count()) {
  304             return $this;
  305         }
  306 
  307         // Check if ordering needs to load the objects.
  308         if (array_diff_key($orderings, $this->getIndexKeys())) {
  309             return $this->__call('orderBy', [$orderings]);
  310         }
  311 
  312         // Ordering can be done by using index only.
  313         $previous = null;
  314         foreach (array_reverse($orderings) as $field => $ordering) {
  315             $field = (string)$field;
  316             if ($this->getKeyField() === $field) {
  317                 $keys = $this->getKeys();
  318                 $search = array_combine($keys, $keys) ?: [];
  319             } elseif ($field === 'flex_key') {
  320                 $search = $this->getFlexKeys();
  321             } else {
  322                 $search = $this->getIndexMap($field);
  323             }
  324 
  325             // Update current search to match the previous ordering.
  326             if (null !== $previous) {
  327                 $search = array_replace($previous, $search);
  328             }
  329 
  330             // Order by current field.
  331             if ($ordering === 'DESC') {
  332                 arsort($search, SORT_NATURAL);
  333             } else {
  334                 asort($search, SORT_NATURAL);
  335             }
  336 
  337             $previous = $search;
  338         }
  339 
  340         return $this->createFrom(array_replace($previous, $this->getEntries()));
  341     }
  342 
  343     /**
  344      * {@inheritDoc}
  345      */
  346     public function call($method, array $arguments = [])
  347     {
  348         return $this->__call('call', [$method, $arguments]);
  349     }
  350 
  351     public function __call($name, $arguments)
  352     {
  353         /** @var Debugger $debugger */
  354         $debugger = Grav::instance()['debugger'];
  355 
  356         /** @var FlexCollection $className */
  357         $className = $this->_flexDirectory->getCollectionClass();
  358         $cachedMethods = $className::getCachedMethods();
  359 
  360         $flexType = $this->getFlexType();
  361 
  362         if (!empty($cachedMethods[$name])) {
  363             $type = $cachedMethods[$name];
  364             if ($type === 'session') {
  365                 /** @var Session $session */
  366                 $session = Grav::instance()['session'];
  367                 $cacheKey = $session->getId() . $session->user->username;
  368             } else {
  369                 $cacheKey = '';
  370             }
  371             $key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey());
  372 
  373             $cache = $this->getCache('object');
  374 
  375             try {
  376                 $result = $cache->get($key);
  377 
  378                 // Make sure the keys aren't changed if the returned type is the same index type.
  379                 if ($result instanceof self && $flexType === $result->getFlexType()) {
  380                     $result = $result->withKeyField($this->getKeyField());
  381                 }
  382             } catch (InvalidArgumentException $e) {
  383                 /** @var Debugger $debugger */
  384                 $debugger = Grav::instance()['debugger'];
  385                 $debugger->addException($e);
  386             }
  387 
  388             if (!isset($result)) {
  389                 $collection = $this->loadCollection();
  390                 $result = $collection->{$name}(...$arguments);
  391 
  392                 try {
  393                     // If flex collection is returned, convert it back to flex index.
  394                     if ($result instanceof FlexCollection) {
  395                         $cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField());
  396                     } else {
  397                         $cached = $result;
  398                     }
  399 
  400                     $cache->set($key, $cached);
  401                 } catch (InvalidArgumentException $e) {
  402                     $debugger->addException($e);
  403 
  404                     // TODO: log error.
  405                 }
  406             }
  407         } else {
  408             $collection = $this->loadCollection();
  409             $result = $collection->{$name}(...$arguments);
  410             if (!isset($cachedMethods[$name])) {
  411                 $class = \get_class($collection);
  412                 $debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug');
  413             }
  414         }
  415 
  416         return $result;
  417     }
  418 
  419     /**
  420      * @return string
  421      */
  422     public function serialize()
  423     {
  424         return serialize(['type' => $this->getFlexType(), 'entries' => $this->getEntries()]);
  425     }
  426 
  427     /**
  428      * @param string $serialized
  429      */
  430     public function unserialize($serialized)
  431     {
  432         $data = unserialize($serialized, ['allowed_classes' => false]);
  433 
  434         $this->_flexDirectory = Grav::instance()['flex_objects']->getDirectory($data['type']);
  435         $this->setEntries($data['entries']);
  436     }
  437 
  438     /**
  439      * @param array $entries
  440      * @param string $keyField
  441      * @return static
  442      */
  443     protected function createFrom(array $entries, string $keyField = null)
  444     {
  445         $index = new static($entries, $this->_flexDirectory);
  446         $index->setKeyField($keyField ?? $this->_keyField);
  447 
  448         return $index;
  449     }
  450 
  451     /**
  452      * @param string|null $keyField
  453      */
  454     protected function setKeyField(string $keyField = null)
  455     {
  456         $this->_keyField = $keyField ?? 'storage_key';
  457     }
  458 
  459     protected function getIndexKeys()
  460     {
  461         if (null === $this->_indexKeys) {
  462             $entries = $this->getEntries();
  463             $first = reset($entries);
  464             if ($first) {
  465                 $keys = array_keys($first);
  466                 $keys = array_combine($keys, $keys) ?: [];
  467             } else {
  468                 $keys = [];
  469             }
  470 
  471             $this->setIndexKeys($keys);
  472         }
  473 
  474         return $this->_indexKeys;
  475     }
  476 
  477     /**
  478      * @param array $indexKeys
  479      */
  480     protected function setIndexKeys(array $indexKeys)
  481     {
  482         // Add defaults.
  483         $indexKeys += [
  484             'key' => 'key',
  485             'storage_key' => 'storage_key',
  486             'storage_timestamp' => 'storage_timestamp',
  487             'flex_key' => 'flex_key'
  488         ];
  489 
  490 
  491         $this->_indexKeys = $indexKeys;
  492     }
  493 
  494     /**
  495      * @return string
  496      */
  497     protected function getTypePrefix()
  498     {
  499         return 'i.';
  500     }
  501 
  502     /**
  503      * @param string $key
  504      * @param mixed $value
  505      * @return ObjectInterface|null
  506      */
  507     protected function loadElement($key, $value): ?ObjectInterface
  508     {
  509         $objects = $this->_flexDirectory->loadObjects([$key => $value]);
  510 
  511         return $objects ? reset($objects): null;
  512     }
  513 
  514     /**
  515      * @param array|null $entries
  516      * @return ObjectInterface[]
  517      */
  518     protected function loadElements(array $entries = null): array
  519     {
  520         return $this->_flexDirectory->loadObjects($entries ?? $this->getEntries());
  521     }
  522 
  523     /**
  524      * @param array|null $entries
  525      * @return ObjectCollectionInterface
  526      */
  527     protected function loadCollection(array $entries = null): CollectionInterface
  528     {
  529         return $this->_flexDirectory->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
  530     }
  531 
  532     /**
  533      * @param mixed $value
  534      * @return bool
  535      */
  536     protected function isAllowedElement($value): bool
  537     {
  538         return $value instanceof FlexObject;
  539     }
  540 
  541     /**
  542      * @param FlexObjectInterface $object
  543      * @return mixed
  544      */
  545     protected function getElementMeta($object)
  546     {
  547         return $object->getTimestamp();
  548     }
  549 
  550     /**
  551      * @param FlexStorageInterface $storage
  552      * @param array $index      Saved index
  553      * @param array $entries    Updated index
  554      * @return array            Compiled list of entries
  555      */
  556     protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries): array
  557     {
  558         // Calculate removed objects.
  559         $removed = array_diff_key($index, $entries);
  560 
  561         // First get rid of all removed objects.
  562         if ($removed) {
  563             $index = array_diff_key($index, $removed);
  564         }
  565 
  566         if ($entries) {
  567             // Calculate difference between saved index and current data.
  568             foreach ($index as $key => $entry) {
  569                 $storage_key = $entry['storage_key'] ?? null;
  570                 if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) {
  571                     // Entry is up to date, no update needed.
  572                     unset($entries[$storage_key]);
  573                 }
  574             }
  575 
  576             if (empty($entries) && empty($removed)) {
  577                 // No objects were added, updated or removed.
  578                 return $index;
  579             }
  580         } elseif (!$removed) {
  581             // There are no objects and nothing was removed.
  582             return [];
  583         }
  584 
  585         // Index should be updated, lock the index file for saving.
  586         $indexFile = static::getIndexFile($storage);
  587         $indexFile->lock();
  588 
  589         // Read all the data rows into an array.
  590         $keys = array_fill_keys(array_keys($entries), null);
  591         $rows = $storage->readRows($keys);
  592 
  593         $keyField = $storage->getKeyField();
  594 
  595         // Go through all the updated objects and refresh their index data.
  596         $updated = $added = [];
  597         foreach ($rows as $key => $row) {
  598             if (null !== $row) {
  599                 $entry = ['key' => $key] + $entries[$key];
  600                 if ($keyField !== 'storage_key' && isset($row[$keyField])) {
  601                     $entry['key'] = $row[$keyField];
  602                 }
  603                 static::updateIndexData($entry, $row);
  604                 if (isset($row['__error'])) {
  605                     $entry['__error'] = true;
  606                     static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error'])));
  607                 }
  608                 if (isset($index[$key])) {
  609                     // Update object in the index.
  610                     $updated[$key] = $entry;
  611                 } else {
  612                     // Add object into the index.
  613                     $added[$key] = $entry;
  614                 }
  615 
  616                 // Either way, update the entry.
  617                 $index[$key] = $entry;
  618             } elseif (isset($index[$key])) {
  619                 // Remove object from the index.
  620                 $removed[$key] = $index[$key];
  621                 unset($index[$key]);
  622             }
  623         }
  624 
  625         // Sort the index before saving it.
  626         ksort($index, SORT_NATURAL);
  627 
  628         static::onChanges($index, $added, $updated, $removed);
  629 
  630         $indexFile->save(['count' => \count($index), 'index' => $index]);
  631         $indexFile->unlock();
  632 
  633         return $index;
  634     }
  635 
  636     protected static function updateIndexData(array &$entry, array $data)
  637     {
  638     }
  639 
  640     protected static function loadEntriesFromIndex(FlexStorageInterface $storage)
  641     {
  642         $indexFile = static::getIndexFile($storage);
  643 
  644         $data = [];
  645         try {
  646             $data = (array)$indexFile->content();
  647         } catch (\Exception $e) {
  648             $e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e);
  649 
  650             static::onException($e);
  651         }
  652 
  653         return $data['index'] ?? [];
  654     }
  655 
  656     protected static function getIndexFile(FlexStorageInterface $storage)
  657     {
  658         // Load saved index file.
  659         $grav = Grav::instance();
  660         $locator = $grav['locator'];
  661         $filename = $locator->findResource($storage->getStoragePath() . '/index.yaml', true, true);
  662 
  663         return CompiledYamlFile::instance($filename);
  664     }
  665 
  666     protected static function onException(\Exception $e)
  667     {
  668         $grav = Grav::instance();
  669 
  670         /** @var Logger $logger */
  671         $logger = $grav['log'];
  672         $logger->addAlert($e->getMessage());
  673 
  674         /** @var Debugger $debugger */
  675         $debugger = $grav['debugger'];
  676         $debugger->addException($e);
  677         $debugger->addMessage($e, 'error');
  678     }
  679 
  680     protected static function onChanges(array $entries, array $added, array $updated, array $removed)
  681     {
  682         $message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed));
  683 
  684         $grav = Grav::instance();
  685 
  686         /** @var Logger $logger */
  687         $logger = $grav['log'];
  688         $logger->addDebug($message);
  689 
  690         /** @var Debugger $debugger */
  691         $debugger = $grav['debugger'];
  692         $debugger->addMessage($message, 'debug');
  693     }
  694 
  695     public function __debugInfo()
  696     {
  697         return [
  698             'type:private' => $this->getFlexType(),
  699             'key:private' => $this->getKey(),
  700             'entries_key:private' => $this->getKeyField(),
  701             'entries:private' => $this->getEntries()
  702         ];
  703     }
  704 }