"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Framework/Flex/FlexDirectory.php" (1 Sep 2020, 19899 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 "FlexDirectory.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\Cache;
   13 use Grav\Common\Config\Config;
   14 use Grav\Common\Data\Blueprint;
   15 use Grav\Common\Debugger;
   16 use Grav\Common\Grav;
   17 use Grav\Common\Utils;
   18 use Grav\Framework\Cache\Adapter\DoctrineCache;
   19 use Grav\Framework\Cache\Adapter\MemoryCache;
   20 use Grav\Framework\Cache\CacheInterface;
   21 use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
   22 use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
   23 use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
   24 use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
   25 use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
   26 use Grav\Framework\Flex\Storage\SimpleStorage;
   27 use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
   28 use Psr\SimpleCache\InvalidArgumentException;
   29 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
   30 use RuntimeException;
   31 
   32 /**
   33  * Class FlexDirectory
   34  * @package Grav\Framework\Flex
   35  */
   36 class FlexDirectory implements FlexAuthorizeInterface
   37 {
   38     use FlexAuthorizeTrait;
   39 
   40     /** @var string */
   41     protected $type;
   42     /** @var string */
   43     protected $blueprint_file;
   44     /** @var Blueprint[] */
   45     protected $blueprints;
   46     /** @var bool[] */
   47     protected $blueprints_init;
   48     /** @var FlexIndexInterface|null */
   49     protected $index;
   50     /** @var FlexCollectionInterface|null */
   51     protected $collection;
   52     /** @var bool */
   53     protected $enabled;
   54     /** @var array */
   55     protected $defaults;
   56     /** @var Config */
   57     protected $config;
   58     /** @var FlexStorageInterface */
   59     protected $storage;
   60     /** @var CacheInterface */
   61     protected $cache;
   62     /** @var string */
   63     protected $objectClassName;
   64     /** @var string */
   65     protected $collectionClassName;
   66     /** @var string */
   67     protected $indexClassName;
   68 
   69     /**
   70      * FlexDirectory constructor.
   71      * @param string $type
   72      * @param string $blueprint_file
   73      * @param array $defaults
   74      */
   75     public function __construct(string $type, string $blueprint_file, array $defaults = [])
   76     {
   77         $this->type = $type;
   78         $this->blueprints = [];
   79         $this->blueprint_file = $blueprint_file;
   80         $this->defaults = $defaults;
   81         $this->enabled = !empty($defaults['enabled']);
   82     }
   83 
   84     /**
   85      * @return bool
   86      */
   87     public function isEnabled(): bool
   88     {
   89         return $this->enabled;
   90     }
   91 
   92     /**
   93      * @return string
   94      * @deprecated 1.6 Use ->getFlexType() method instead.
   95      */
   96     public function getType(): string
   97     {
   98         user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
   99 
  100         return $this->type;
  101     }
  102 
  103     /**
  104      * @return string
  105      */
  106     public function getFlexType(): string
  107     {
  108         return $this->type;
  109     }
  110 
  111     /**
  112      * @return string
  113      */
  114     public function getTitle(): string
  115     {
  116         return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType()));
  117     }
  118 
  119     /**
  120      * @return string
  121      */
  122     public function getDescription(): string
  123     {
  124         return $this->getBlueprintInternal()->get('description', '');
  125     }
  126 
  127     /**
  128      * @param string|null $name
  129      * @param mixed $default
  130      * @return mixed
  131      */
  132     public function getConfig(string $name = null, $default = null)
  133     {
  134         if (null === $this->config) {
  135             $this->config = new Config(array_merge_recursive($this->getBlueprintInternal()->get('config', []), $this->defaults));
  136         }
  137 
  138         return null === $name ? $this->config : $this->config->get($name, $default);
  139     }
  140 
  141     /**
  142      * @param string $type
  143      * @param string $context
  144      * @return Blueprint
  145      */
  146     public function getBlueprint(string $type = '', string $context = '')
  147     {
  148         $blueprint = $this->getBlueprintInternal($type, $context);
  149 
  150         if (empty($this->blueprints_init[$type])) {
  151             $this->blueprints_init[$type] = true;
  152 
  153             $blueprint->setScope('object');
  154             $blueprint->init();
  155             if (empty($blueprint->fields())) {
  156                 throw new RuntimeException(sprintf('Flex: Blueprint for %s is missing', $this->type));
  157             }
  158         }
  159 
  160         return $blueprint;
  161     }
  162 
  163     /**
  164      * @param string $view
  165      * @return string
  166      */
  167     public function getBlueprintFile(string $view = ''): string
  168     {
  169         $file = $this->blueprint_file;
  170         if ($view !== '') {
  171             $file = preg_replace('/\.yaml/', "/{$view}.yaml", $file);
  172         }
  173 
  174         return $file;
  175     }
  176 
  177     /**
  178      * Get collection. In the site this will be filtered by the default filters (published etc).
  179      *
  180      * Use $directory->getIndex() if you want unfiltered collection.
  181      *
  182      * @param array|null $keys  Array of keys.
  183      * @param string|null $keyField  Field to be used as the key.
  184      * @return FlexCollectionInterface
  185      */
  186     public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface
  187     {
  188         // Get all selected entries.
  189         $index = $this->getIndex($keys, $keyField);
  190 
  191         if (!Utils::isAdminPlugin()) {
  192             // If not in admin, filter the list by using default filters.
  193             $filters = (array)$this->getConfig('site.filter', []);
  194 
  195             foreach ($filters as $filter) {
  196                 $index = $index->{$filter}();
  197             }
  198         }
  199 
  200         return $index;
  201     }
  202 
  203     /**
  204      * Get the full collection of all stored objects.
  205      *
  206      * Use $directory->getCollection() if you want a filtered collection.
  207      *
  208      * @param array|null $keys  Array of keys.
  209      * @param string|null $keyField  Field to be used as the key.
  210      * @return FlexIndexInterface
  211      */
  212     public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface
  213     {
  214         $index = clone $this->loadIndex();
  215         $index = $index->withKeyField($keyField);
  216 
  217         if (null !== $keys) {
  218             $index = $index->select($keys);
  219         }
  220 
  221         return $index->getIndex();
  222     }
  223 
  224     /**
  225      * Returns an object if it exists.
  226      *
  227      * Note: It is not safe to use the object without checking if the user can access it.
  228      *
  229      * @param string $key
  230      * @param string|null $keyField  Field to be used as the key.
  231      * @return FlexObjectInterface|null
  232      */
  233     public function getObject($key, string $keyField = null): ?FlexObjectInterface
  234     {
  235         return $this->getIndex(null, $keyField)->get($key);
  236     }
  237 
  238     /**
  239      * @param array $data
  240      * @param string|null $key
  241      * @return FlexObjectInterface
  242      */
  243     public function update(array $data, string $key = null): FlexObjectInterface
  244     {
  245         $object = null !== $key ? $this->getIndex()->get($key): null;
  246 
  247         $storage = $this->getStorage();
  248 
  249         if (null === $object) {
  250             $object = $this->createObject($data, $key, true);
  251             $key = $object->getStorageKey();
  252 
  253             if ($key) {
  254                 $rows = $storage->replaceRows([$key => $object->prepareStorage()]);
  255             } else {
  256                 $rows = $storage->createRows([$object->prepareStorage()]);
  257             }
  258         } else {
  259             $oldKey = $object->getStorageKey();
  260             $object->update($data);
  261             $newKey = $object->getStorageKey();
  262 
  263             if ($oldKey !== $newKey) {
  264                 $object->triggerEvent('move');
  265                 $storage->renameRow($oldKey, $newKey);
  266                 // TODO: media support.
  267             }
  268 
  269             $object->save();
  270         }
  271 
  272         try {
  273             $this->clearCache();
  274         } catch (InvalidArgumentException $e) {
  275             /** @var Debugger $debugger */
  276             $debugger = Grav::instance()['debugger'];
  277             $debugger->addException($e);
  278 
  279             // Caching failed, but we can ignore that for now.
  280         }
  281 
  282         return $object;
  283     }
  284 
  285     /**
  286      * @param string $key
  287      * @return FlexObjectInterface|null
  288      */
  289     public function remove(string $key): ?FlexObjectInterface
  290     {
  291         $object = $this->getIndex()->get($key);
  292         if (!$object) {
  293             return null;
  294         }
  295 
  296         $object->delete();
  297 
  298         return $object;
  299     }
  300 
  301     /**
  302      * @param string|null $namespace
  303      * @return CacheInterface
  304      */
  305     public function getCache(string $namespace = null)
  306     {
  307         $namespace = $namespace ?: 'index';
  308         $cache = $this->cache[$namespace] ?? null;
  309 
  310         if (null === $cache) {
  311             try {
  312                 $grav = Grav::instance();
  313 
  314                 /** @var Cache $gravCache */
  315                 $gravCache = $grav['cache'];
  316                 $config = $this->getConfig('cache.' . $namespace);
  317                 if (empty($config['enabled'])) {
  318                     $cache = new MemoryCache('flex-objects-' . $this->getFlexType());
  319                 } else {
  320                     $timeout = $config['timeout'] ?? 60;
  321 
  322                     $key = $gravCache->getKey();
  323                     if (Utils::isAdminPlugin()) {
  324                         $key = substr($key, 0, -1);
  325                     }
  326                     $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout);
  327                 }
  328             } catch (\Exception $e) {
  329                 /** @var Debugger $debugger */
  330                 $debugger = Grav::instance()['debugger'];
  331                 $debugger->addException($e);
  332 
  333                 $cache = new MemoryCache('flex-objects-' . $this->getFlexType());
  334             }
  335 
  336             // Disable cache key validation.
  337             $cache->setValidation(false);
  338             $this->cache[$namespace] = $cache;
  339         }
  340 
  341         return $cache;
  342     }
  343 
  344     /**
  345      * @return $this
  346      */
  347     public function clearCache()
  348     {
  349         $grav = Grav::instance();
  350 
  351         /** @var Debugger $debugger */
  352         $debugger = $grav['debugger'];
  353         $debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug');
  354 
  355         /** @var UniformResourceLocator $locator */
  356         $locator = $grav['locator'];
  357         $locator->clearCache();
  358 
  359         $this->getCache('index')->clear();
  360         $this->getCache('object')->clear();
  361         $this->getCache('render')->clear();
  362 
  363         $this->index = null;
  364 
  365         return $this;
  366     }
  367 
  368     /**
  369      * @param string|null $key
  370      * @return string
  371      */
  372     public function getStorageFolder(string $key = null): string
  373     {
  374         return $this->getStorage()->getStoragePath($key);
  375     }
  376 
  377     /**
  378      * @param string|null $key
  379      * @return string
  380      */
  381     public function getMediaFolder(string $key = null): string
  382     {
  383         return $this->getStorage()->getMediaPath($key);
  384     }
  385 
  386     /**
  387      * @return FlexStorageInterface
  388      */
  389     public function getStorage(): FlexStorageInterface
  390     {
  391         if (null === $this->storage) {
  392             $this->storage = $this->createStorage();
  393         }
  394 
  395         return $this->storage;
  396     }
  397 
  398     /**
  399      * @param array $data
  400      * @param string $key
  401      * @param bool $validate
  402      * @return FlexObjectInterface
  403      */
  404     public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface
  405     {
  406         /** @var string|FlexObjectInterface $className */
  407         $className = $this->objectClassName ?: $this->getObjectClass();
  408 
  409         return new $className($data, $key, $this, $validate);
  410     }
  411 
  412     /**
  413      * @param array $entries
  414      * @param string $keyField
  415      * @return FlexCollectionInterface
  416      */
  417     public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface
  418     {
  419         /** @var string|FlexCollectionInterface $className */
  420         $className = $this->collectionClassName ?: $this->getCollectionClass();
  421 
  422         return $className::createFromArray($entries, $this, $keyField);
  423     }
  424 
  425     /**
  426      * @param array $entries
  427      * @param string $keyField
  428      * @return FlexIndexInterface
  429      */
  430     public function createIndex(array $entries, string $keyField = null): FlexIndexInterface
  431     {
  432         /** @var string|FlexIndexInterface $className */
  433         $className = $this->indexClassName ?: $this->getIndexClass();
  434 
  435         return $className::createFromArray($entries, $this, $keyField);
  436     }
  437 
  438     /**
  439      * @return string
  440      */
  441     public function getObjectClass(): string
  442     {
  443         if (!$this->objectClassName) {
  444             $this->objectClassName = $this->getConfig('data.object', 'Grav\\Framework\\Flex\\FlexObject');
  445         }
  446 
  447         return $this->objectClassName;
  448 
  449     }
  450 
  451     /**
  452      * @return string
  453      */
  454     public function getCollectionClass(): string
  455     {
  456         if (!$this->collectionClassName) {
  457             $this->collectionClassName = $this->getConfig('data.collection', 'Grav\\Framework\\Flex\\FlexCollection');
  458         }
  459 
  460         return $this->collectionClassName;
  461     }
  462 
  463 
  464     /**
  465      * @return string
  466      */
  467     public function getIndexClass(): string
  468     {
  469         if (!$this->indexClassName) {
  470             $this->indexClassName = $this->getConfig('data.index', 'Grav\\Framework\\Flex\\FlexIndex');
  471         }
  472 
  473         return $this->indexClassName;
  474     }
  475 
  476     /**
  477      * @param array $entries
  478      * @param string $keyField
  479      * @return FlexCollectionInterface
  480      */
  481     public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface
  482     {
  483         return $this->createCollection($this->loadObjects($entries), $keyField);
  484     }
  485 
  486     /**
  487      * @param array $entries
  488      * @return FlexObjectInterface[]
  489      * @internal
  490      */
  491     public function loadObjects(array $entries): array
  492     {
  493         /** @var Debugger $debugger */
  494         $debugger = Grav::instance()['debugger'];
  495         $debugger->startTimer('flex-objects', sprintf('Flex: Initializing %d %s', \count($entries), $this->type));
  496 
  497         $storage = $this->getStorage();
  498         $cache = $this->getCache('object');
  499 
  500         // Get storage keys for the objects.
  501         $keys = [];
  502         $rows = [];
  503         foreach ($entries as $key => $value) {
  504             $k = $value['storage_key'];
  505             $keys[$k] = $key;
  506             $rows[$k] = null;
  507         }
  508 
  509         // Fetch rows from the cache.
  510         try {
  511             $rows = $cache->getMultiple(array_keys($rows));
  512         } catch (InvalidArgumentException $e) {
  513             $debugger->addException($e);
  514         }
  515 
  516         // Read missing rows from the storage.
  517         $updated = [];
  518         $rows = $storage->readRows($rows, $updated);
  519 
  520         // Store updated rows to the cache.
  521         if ($updated) {
  522             try {
  523                 if (!$cache instanceof MemoryCache) {
  524                     $debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug');
  525                 }
  526                 $cache->setMultiple($updated);
  527             } catch (InvalidArgumentException $e) {
  528                 $debugger->addException($e);
  529 
  530                 // TODO: log about the issue.
  531             }
  532         }
  533 
  534         // Create objects from the rows.
  535         $list = [];
  536         foreach ($rows as $storageKey => $row) {
  537             if ($row === null) {
  538                 $debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
  539                 continue;
  540             }
  541 
  542             if (isset($row['__error'])) {
  543                 $message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__error']);
  544                 $debugger->addException(new \RuntimeException($message));
  545                 $debugger->addMessage($message, 'error');
  546                 continue;
  547             }
  548 
  549             $usedKey = $keys[$storageKey];
  550             $row += [
  551                 'storage_key' => $storageKey,
  552                 'storage_timestamp' => $entries[$usedKey]['storage_timestamp'],
  553             ];
  554 
  555             $key = $entries[$usedKey]['key'] ?? $usedKey;
  556             $object = $this->createObject($row, $key, false);
  557             $list[$usedKey] = $object;
  558         }
  559 
  560         $debugger->stopTimer('flex-objects');
  561 
  562         return $list;
  563     }
  564 
  565     /**
  566      * @param string $type_view
  567      * @param string $context
  568      * @return Blueprint
  569      */
  570     protected function getBlueprintInternal(string $type_view = '', string $context = '')
  571     {
  572         if (!isset($this->blueprints[$type_view])) {
  573             if (!file_exists($this->blueprint_file)) {
  574                 throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type));
  575             }
  576 
  577             $parts = explode('.', rtrim($type_view, '.'), 2);
  578             $type = array_shift($parts);
  579             $view = array_shift($parts) ?: '';
  580 
  581             $blueprint = new Blueprint($this->getBlueprintFile($view));
  582             if ($context) {
  583                 $blueprint->setContext($context);
  584             }
  585 
  586             $blueprint->load($type ?: null);
  587             if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) {
  588                 $blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load();
  589                 $blueprint->extend($blueprintBase, true);
  590             }
  591 
  592             $this->blueprints[$type_view] = $blueprint;
  593         }
  594 
  595         return $this->blueprints[$type_view];
  596     }
  597 
  598     /**
  599      * @return FlexStorageInterface
  600      */
  601     protected function createStorage(): FlexStorageInterface
  602     {
  603         $this->collection = $this->createCollection([]);
  604 
  605         $storage = $this->getConfig('data.storage');
  606 
  607         if (!\is_array($storage)) {
  608             $storage = ['options' => ['folder' => $storage]];
  609         }
  610 
  611         $className = $storage['class'] ?? SimpleStorage::class;
  612         $options = $storage['options'] ?? [];
  613 
  614         return new $className($options);
  615     }
  616 
  617     /**
  618      * @return FlexIndexInterface
  619      */
  620     protected function loadIndex(): FlexIndexInterface
  621     {
  622         static $i = 0;
  623 
  624         $index = $this->index;
  625 
  626         if (null === $index) {
  627             $i++; $j = $i;
  628             /** @var Debugger $debugger */
  629             $debugger = Grav::instance()['debugger'];
  630             $debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index");
  631 
  632             $storage = $this->getStorage();
  633             $cache = $this->getCache('index');
  634 
  635             try {
  636                 $keys = $cache->get('__keys');
  637             } catch (InvalidArgumentException $e) {
  638                 $debugger->addException($e);
  639                 $keys = null;
  640             }
  641 
  642             if (null === $keys) {
  643                 /** @var string|FlexIndexInterface $className */
  644                 $className = $this->getIndexClass();
  645                 $keys = $className::loadEntriesFromStorage($storage);
  646                 if (!$cache instanceof MemoryCache) {
  647                     $debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)),
  648                         'debug');
  649                 }
  650                 try {
  651                     $cache->set('__keys', $keys);
  652                 } catch (InvalidArgumentException $e) {
  653                     $debugger->addException($e);
  654                     // TODO: log about the issue.
  655                 }
  656             }
  657 
  658             // We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop.
  659             $this->index = $this->createIndex($keys);
  660             /** @var FlexCollectionInterface $collection */
  661             $collection = $this->index->orderBy($this->getConfig('data.ordering', []));
  662             $this->index = $index = $collection->getIndex();
  663 
  664             $debugger->stopTimer('flex-keys-' . $this->type . $j);
  665         }
  666 
  667         return $index;
  668     }
  669 }