"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Framework/Flex/FlexObject.php" (1 Sep 2020, 24449 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 "FlexObject.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\Data\Blueprint;
   13 use Grav\Common\Debugger;
   14 use Grav\Common\Grav;
   15 use Grav\Common\Twig\Twig;
   16 use Grav\Common\Utils;
   17 use Grav\Framework\Cache\CacheInterface;
   18 use Grav\Framework\ContentBlock\HtmlBlock;
   19 use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
   20 use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
   21 use Grav\Framework\Flex\Interfaces\FlexFormInterface;
   22 use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
   23 use Grav\Framework\Object\Access\NestedArrayAccessTrait;
   24 use Grav\Framework\Object\Access\NestedPropertyTrait;
   25 use Grav\Framework\Object\Access\OverloadedPropertyTrait;
   26 use Grav\Framework\Object\Base\ObjectTrait;
   27 use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
   28 use Grav\Framework\Object\Interfaces\ObjectInterface;
   29 use Grav\Framework\Object\Property\LazyPropertyTrait;
   30 use Psr\SimpleCache\InvalidArgumentException;
   31 use RocketTheme\Toolbox\Event\Event;
   32 use Twig\Error\LoaderError;
   33 use Twig\Error\SyntaxError;
   34 use Twig\Template;
   35 use Twig\TemplateWrapper;
   36 
   37 /**
   38  * Class FlexObject
   39  * @package Grav\Framework\Flex
   40  */
   41 class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
   42 {
   43     use ObjectTrait;
   44     use LazyPropertyTrait {
   45         LazyPropertyTrait::__construct as private objectConstruct;
   46     }
   47     use NestedPropertyTrait;
   48     use OverloadedPropertyTrait;
   49     use NestedArrayAccessTrait;
   50     use FlexAuthorizeTrait;
   51 
   52     /** @var FlexDirectory */
   53     private $_flexDirectory;
   54     /** @var FlexFormInterface[] */
   55     private $_forms = [];
   56     /** @var array */
   57     private $_storage;
   58     /** @var array */
   59     protected $_changes;
   60 
   61     /**
   62      * @return array
   63      */
   64     public static function getCachedMethods(): array
   65     {
   66         return [
   67             'getTypePrefix' => true,
   68             'getType' => true,
   69             'getFlexType' => true,
   70             'getFlexDirectory' => true,
   71             'getCacheKey' => true,
   72             'getCacheChecksum' => true,
   73             'getTimestamp' => true,
   74             'value' => true,
   75             'exists' => true,
   76             'hasProperty' => true,
   77             'getProperty' => true,
   78 
   79             // FlexAclTrait
   80             'isAuthorized' => 'session',
   81         ];
   82     }
   83 
   84     public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false)
   85     {
   86         $instance = new static($elements, $storage['key'], $directory, $validate);
   87         $instance->setStorage($storage);
   88 
   89         return $instance;
   90     }
   91 
   92     /**
   93      * {@inheritdoc}
   94      * @see FlexObjectInterface::__construct()
   95      */
   96     public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
   97     {
   98         $this->_flexDirectory = $directory;
   99 
  100         if ($validate) {
  101             $blueprint = $this->getFlexDirectory()->getBlueprint();
  102 
  103             $blueprint->validate($elements);
  104 
  105             $elements = $blueprint->filter($elements);
  106         }
  107 
  108         $this->filterElements($elements);
  109 
  110         $this->objectConstruct($elements, $key);
  111     }
  112 
  113     /**
  114      * {@inheritdoc}
  115      * @see FlexObjectInterface::getFlexType()
  116      */
  117     public function getFlexType(): string
  118     {
  119         return $this->_flexDirectory->getFlexType();
  120     }
  121 
  122     /**
  123      * {@inheritdoc}
  124      * @see FlexObjectInterface::getFlexDirectory()
  125      */
  126     public function getFlexDirectory(): FlexDirectory
  127     {
  128         return $this->_flexDirectory;
  129     }
  130 
  131     /**
  132      * {@inheritdoc}
  133      * @see FlexObjectInterface::getTimestamp()
  134      */
  135     public function getTimestamp(): int
  136     {
  137         return $this->_storage['storage_timestamp'] ?? 0;
  138     }
  139 
  140     /**
  141      * {@inheritdoc}
  142      * @see FlexObjectInterface::getCacheKey()
  143      */
  144     public function getCacheKey(): string
  145     {
  146         return $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getStorageKey();
  147     }
  148 
  149     /**
  150      * {@inheritdoc}
  151      * @see FlexObjectInterface::getCacheChecksum()
  152      */
  153     public function getCacheChecksum(): string
  154     {
  155         return (string)$this->getTimestamp();
  156     }
  157 
  158     /**
  159      * {@inheritdoc}
  160      * @see FlexObjectInterface::search()
  161      */
  162     public function search(string $search, $properties = null, array $options = null): float
  163     {
  164         $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
  165         $properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []);
  166         if (!$properties) {
  167             foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) {
  168                 if (!empty($value['link'])) {
  169                     $properties[] = $property;
  170                 }
  171             }
  172         }
  173 
  174         $weight = 0;
  175         foreach ((array)$properties as $property) {
  176             $weight += $this->searchNestedProperty($property, $search, $options);
  177         }
  178 
  179         return $weight > 0 ? min($weight, 1) : 0;
  180     }
  181 
  182     /**
  183      * {@inheritdoc}
  184      * @see ObjectInterface::getFlexKey()
  185      */
  186     public function getKey()
  187     {
  188         return $this->_key ?: $this->getFlexType() . '@@' . spl_object_hash($this);
  189     }
  190 
  191     /**
  192      * {@inheritdoc}
  193      * @see FlexObjectInterface::getFlexKey()
  194      */
  195     public function getFlexKey(): string
  196     {
  197         return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey();
  198     }
  199 
  200     /**
  201      * {@inheritdoc}
  202      * @see FlexObjectInterface::getStorageKey()
  203      */
  204     public function getStorageKey(): string
  205     {
  206         return $this->_storage['storage_key'] ?? $this->getTypePrefix() . $this->getFlexType() . '@@' . spl_object_hash($this);
  207     }
  208 
  209     /**
  210      * {@inheritdoc}
  211      * @see FlexObjectInterface::getMetaData()
  212      */
  213     public function getMetaData(): array
  214     {
  215         return $this->getStorage();
  216     }
  217 
  218     /**
  219      * {@inheritdoc}
  220      * @see FlexObjectInterface::exists()
  221      */
  222     public function exists(): bool
  223     {
  224         $key = $this->getStorageKey();
  225 
  226         return $key && $this->getFlexDirectory()->getStorage()->hasKey($key);
  227     }
  228 
  229     /**
  230      * @param string $property
  231      * @param string $search
  232      * @param array|null $options
  233      * @return float
  234      */
  235     public function searchProperty(string $property, string $search, array $options = null): float
  236     {
  237         $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
  238         $value = $this->getProperty($property);
  239 
  240         return $this->searchValue($property, $value, $search, $options);
  241     }
  242 
  243     /**
  244      * @param string $property
  245      * @param string $search
  246      * @param array|null $options
  247      * @return float
  248      */
  249     public function searchNestedProperty(string $property, string $search, array $options = null): float
  250     {
  251         $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
  252         $value = $this->getNestedProperty($property);
  253 
  254         return $this->searchValue($property, $value, $search, $options);
  255     }
  256 
  257     /**
  258      * @param string $name
  259      * @param mixed $value
  260      * @param string $search
  261      * @param array|null $options
  262      * @return float
  263      */
  264     protected function searchValue(string $name, $value, string $search, array $options = null): float
  265     {
  266         $search = trim($search);
  267 
  268         if ($search === '') {
  269             return 0;
  270         }
  271 
  272         if (!\is_string($value) || $value === '') {
  273             return 0;
  274         }
  275 
  276         $tested = false;
  277         if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $options['case_sensitive'] ?? false)) {
  278             return (float)$options['starts_with'];
  279         }
  280         if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $options['case_sensitive'] ?? false)) {
  281             return (float)$options['ends_with'];
  282         }
  283         if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) {
  284             return (float)($options['contains'] ?? 1);
  285         }
  286 
  287         return 0;
  288     }
  289 
  290     /**
  291      * Get any changes based on data sent to update
  292      *
  293      * @return array
  294      */
  295     public function getChanges(): array
  296     {
  297         return $this->_changes ?? [];
  298     }
  299 
  300     /**
  301      * @return string
  302      */
  303     protected function getTypePrefix(): string
  304     {
  305         return 'o.';
  306     }
  307 
  308     /**
  309      * @param bool $prefix
  310      * @return string
  311      * @deprecated 1.6 Use `->getFlexType()` instead.
  312      */
  313     public function getType($prefix = false)
  314     {
  315         user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  316 
  317         $type = $prefix ? $this->getTypePrefix() : '';
  318 
  319         return $type . $this->getFlexType();
  320     }
  321 
  322     /**
  323      * Alias of getBlueprint()
  324      *
  325      * @return Blueprint
  326      * @deprecated 1.6 Admin compatibility
  327      */
  328     public function blueprints()
  329     {
  330         return $this->getBlueprint();
  331     }
  332 
  333     /**
  334      * @param string|null $namespace
  335      * @return CacheInterface
  336      */
  337     public function getCache(string $namespace = null)
  338     {
  339         return $this->_flexDirectory->getCache($namespace);
  340     }
  341 
  342     /**
  343      * @param string|null $key
  344      * @return $this
  345      */
  346     public function setStorageKey($key = null)
  347     {
  348         $this->_storage['storage_key'] = $key;
  349 
  350         return $this;
  351     }
  352 
  353     /**
  354      * @param int $timestamp
  355      * @return $this
  356      */
  357     public function setTimestamp($timestamp = null)
  358     {
  359         $this->_storage['storage_timestamp'] = $timestamp ?? time();
  360 
  361         return $this;
  362     }
  363 
  364     /**
  365      * {@inheritdoc}
  366      * @see FlexObjectInterface::render()
  367      */
  368     public function render(string $layout = null, array $context = [])
  369     {
  370         if (null === $layout) {
  371             $layout = 'default';
  372         }
  373 
  374         $type = $this->getFlexType();
  375 
  376         $grav = Grav::instance();
  377 
  378         /** @var Debugger $debugger */
  379         $debugger = $grav['debugger'];
  380         $debugger->startTimer('flex-object-' . ($debugKey =  uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')');
  381 
  382         $cache = $key = null;
  383         foreach ($context as $value) {
  384             if (!\is_scalar($value)) {
  385                 $key = false;
  386             }
  387         }
  388 
  389         if ($key !== false) {
  390             $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
  391             $cache = $this->getCache('render');
  392         }
  393 
  394         try {
  395             $data = $cache && $key ? $cache->get($key) : null;
  396 
  397             $block = $data ? HtmlBlock::fromArray($data) : null;
  398         } catch (InvalidArgumentException $e) {
  399             $debugger->addException($e);
  400 
  401             $block = null;
  402         } catch (\InvalidArgumentException $e) {
  403             $debugger->addException($e);
  404 
  405             $block = null;
  406         }
  407 
  408         $checksum = $this->getCacheChecksum();
  409         if ($block && $checksum !== $block->getChecksum()) {
  410             $block = null;
  411         }
  412 
  413         if (!$block) {
  414             $block = HtmlBlock::create($key ?: null);
  415             $block->setChecksum($checksum);
  416             if ($key === false) {
  417                 $block->disableCache();
  418             }
  419 
  420             $grav->fireEvent('onFlexObjectRender', new Event([
  421                 'object' => $this,
  422                 'layout' => &$layout,
  423                 'context' => &$context
  424             ]));
  425 
  426             $output = $this->getTemplate($layout)->render(
  427                 ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'object' => $this, 'layout' => $layout] + $context
  428             );
  429 
  430             if ($debugger->enabled()) {
  431                 $name = $this->getKey() . ' (' . $type . ')';
  432                 $output = "\n<!–– START {$name} object ––>\n{$output}\n<!–– END {$name} object ––>\n";
  433             }
  434 
  435             $block->setContent($output);
  436 
  437             try {
  438                 $cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
  439             } catch (InvalidArgumentException $e) {
  440                 $debugger->addException($e);
  441             }
  442         }
  443 
  444         $debugger->stopTimer('flex-object-' . $debugKey);
  445 
  446         return $block;
  447     }
  448 
  449     /**
  450      * @return array
  451      */
  452     public function jsonSerialize()
  453     {
  454         return $this->getElements();
  455     }
  456 
  457     /**
  458      * {@inheritdoc}
  459      * @see FlexObjectInterface::prepareStorage()
  460      */
  461     public function prepareStorage(): array
  462     {
  463         return $this->getElements();
  464     }
  465 
  466     /**
  467      * @param string $name
  468      * @return $this
  469      */
  470     public function triggerEvent($name)
  471     {
  472         return $this;
  473     }
  474 
  475     /**
  476      * {@inheritdoc}
  477      * @see FlexObjectInterface::update()
  478      */
  479     public function update(array $data, array $files = [])
  480     {
  481         if ($data) {
  482             $blueprint = $this->getBlueprint();
  483 
  484             // Process updated data through the object filters.
  485             $this->filterElements($data);
  486 
  487             // Get currently stored data.
  488             $elements = $this->getElements();
  489 
  490             // Merge existing object to the test data to be validated.
  491             $test = $blueprint->mergeData($elements, $data);
  492 
  493             // Validate and filter elements and throw an error if any issues were found.
  494             $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]);
  495             $data = $blueprint->filter($data, false, true);
  496 
  497             // Finally update the object.
  498             foreach ($blueprint->flattenData($data) as $key => $value) {
  499                 if ($value === null) {
  500                     $this->unsetNestedProperty($key);
  501                 } else {
  502                     $this->setNestedProperty($key, $value);
  503                 }
  504             }
  505 
  506             // Store the changes
  507             $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
  508         }
  509 
  510         if ($files && method_exists($this, 'setUpdatedMedia')) {
  511             $this->setUpdatedMedia($files);
  512         }
  513 
  514         return $this;
  515     }
  516 
  517     /**
  518      * {@inheritdoc}
  519      * @see FlexObjectInterface::create()
  520      */
  521     public function create(string $key = null)
  522     {
  523         if ($key) {
  524             $this->setStorageKey($key);
  525         }
  526 
  527         if ($this->exists()) {
  528             throw new \RuntimeException('Cannot create new object (Already exists)');
  529         }
  530 
  531         return $this->save();
  532     }
  533 
  534     /**
  535      * {@inheritdoc}
  536      * @see FlexObjectInterface::save()
  537      */
  538     public function save()
  539     {
  540         $this->triggerEvent('onBeforeSave');
  541 
  542         $result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
  543 
  544         $value = reset($result);
  545         $storageKey = (string)key($result);
  546         if ($value && $storageKey) {
  547             $this->setStorageKey($storageKey);
  548             if (!$this->hasKey()) {
  549                 $this->setKey($storageKey);
  550             }
  551         }
  552 
  553         // FIXME: For some reason locator caching isn't cleared for the file, investigate!
  554         $locator = Grav::instance()['locator'];
  555         $locator->clearCache();
  556 
  557         // Make sure that the object exists before continuing (just in case).
  558         if (!$this->exists()) {
  559             throw new \RuntimeException('Saving failed: Object does not exist!');
  560         }
  561 
  562         if (method_exists($this, 'saveUpdatedMedia')) {
  563             $this->saveUpdatedMedia();
  564         }
  565 
  566         try {
  567             $this->getFlexDirectory()->clearCache();
  568             if (method_exists($this, 'clearMediaCache')) {
  569                 $this->clearMediaCache();
  570             }
  571         } catch (\Exception $e) {
  572             /** @var Debugger $debugger */
  573             $debugger = Grav::instance()['debugger'];
  574             $debugger->addException($e);
  575 
  576             // Caching failed, but we can ignore that for now.
  577         }
  578 
  579         $this->triggerEvent('onAfterSave');
  580 
  581         return $this;
  582     }
  583 
  584     /**
  585      * {@inheritdoc}
  586      * @see FlexObjectInterface::delete()
  587      */
  588     public function delete()
  589     {
  590         $this->triggerEvent('onBeforeDelete');
  591 
  592         $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
  593 
  594         try {
  595             $this->getFlexDirectory()->clearCache();
  596             if (method_exists($this, 'clearMediaCache')) {
  597                 $this->clearMediaCache();
  598             }
  599         } catch (\Exception $e) {
  600             /** @var Debugger $debugger */
  601             $debugger = Grav::instance()['debugger'];
  602             $debugger->addException($e);
  603 
  604             // Caching failed, but we can ignore that for now.
  605         }
  606 
  607         $this->triggerEvent('onAfterDelete');
  608 
  609         return $this;
  610     }
  611 
  612     /**
  613      * {@inheritdoc}
  614      * @see FlexObjectInterface::getBlueprint()
  615      */
  616     public function getBlueprint(string $name = '')
  617     {
  618         return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name);
  619     }
  620 
  621     /**
  622      * {@inheritdoc}
  623      * @see FlexObjectInterface::getForm()
  624      */
  625     public function getForm(string $name = '', array $form = null)
  626     {
  627         if (!isset($this->_forms[$name])) {
  628             $this->_forms[$name] = $this->createFormObject($name, $form);
  629         }
  630 
  631         return $this->_forms[$name];
  632     }
  633 
  634     /**
  635      * {@inheritdoc}
  636      * @see FlexObjectInterface::getDefaultValue()
  637      */
  638     public function getDefaultValue(string $name, string $separator = null)
  639     {
  640         $separator = $separator ?: '.';
  641         $path = explode($separator, $name) ?: [];
  642         $offset = array_shift($path) ?? '';
  643 
  644         $current = $this->getDefaultValues();
  645 
  646         if (!isset($current[$offset])) {
  647             return null;
  648         }
  649 
  650         $current = $current[$offset];
  651 
  652         while ($path) {
  653             $offset = array_shift($path);
  654 
  655             if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
  656                 $current = $current[$offset];
  657             } elseif (\is_object($current) && isset($current->{$offset})) {
  658                 $current = $current->{$offset};
  659             } else {
  660                 return null;
  661             }
  662         };
  663 
  664         return $current;
  665     }
  666 
  667     /**
  668      * @return array
  669      */
  670     public function getDefaultValues(): array
  671     {
  672         return $this->getBlueprint()->getDefaults();
  673     }
  674 
  675     /**
  676      * {@inheritdoc}
  677      * @see FlexObjectInterface::getFormValue()
  678      */
  679     public function getFormValue(string $name, $default = null, string $separator = null)
  680     {
  681         if ($name === 'storage_key') {
  682             return $this->getStorageKey();
  683         }
  684         if ($name === 'storage_timestamp') {
  685             return $this->getTimestamp();
  686         }
  687 
  688         return $this->getNestedProperty($name, $default, $separator);
  689     }
  690 
  691     /**
  692      * @param string $name
  693      * @param mixed|null $default
  694      * @param string|null $separator
  695      * @return mixed
  696      *
  697      * @deprecated 1.6 Use ->getFormValue() method instead.
  698      */
  699     public function value($name, $default = null, $separator = null)
  700     {
  701         return $this->getFormValue($name, $default, $separator);
  702     }
  703 
  704     /**
  705      * Returns a string representation of this object.
  706      *
  707      * @return string
  708      */
  709     public function __toString()
  710     {
  711         return $this->getFlexKey();
  712     }
  713 
  714     public function __debugInfo()
  715     {
  716         return [
  717             'type:private' => $this->getFlexType(),
  718             'key:private' => $this->getKey(),
  719             'elements:private' => $this->getElements(),
  720             'storage:private' => $this->getStorage()
  721         ];
  722     }
  723 
  724     /**
  725      * @return array
  726      */
  727     protected function doSerialize(): array
  728     {
  729         return [
  730             'type' => $this->getFlexType(),
  731             'key' => $this->getKey(),
  732             'elements' => $this->getElements(),
  733             'storage' => $this->getStorage()
  734         ];
  735     }
  736 
  737     /**
  738      * @param array $serialized
  739      */
  740     protected function doUnserialize(array $serialized): void
  741     {
  742         $type = $serialized['type'] ?? 'unknown';
  743 
  744         if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) {
  745             throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data");
  746         }
  747 
  748         $grav = Grav::instance();
  749         /** @var Flex|null $flex */
  750         $flex = $grav['flex_objects'] ?? null;
  751         $directory = $flex ? $flex->getDirectory($type) : null;
  752         if (!$directory) {
  753             throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found");
  754         }
  755         $this->setFlexDirectory($directory);
  756         $this->setStorage($serialized['storage']);
  757         $this->setKey($serialized['key']);
  758         $this->setElements($serialized['elements']);
  759     }
  760 
  761     /**
  762      * @param FlexDirectory $directory
  763      */
  764     public function setFlexDirectory(FlexDirectory $directory): void
  765     {
  766         $this->_flexDirectory = $directory;
  767     }
  768     /**
  769      * @param array $storage
  770      */
  771     protected function setStorage(array $storage) : void
  772     {
  773         $this->_storage = $storage;
  774     }
  775 
  776     /**
  777      * @return array
  778      */
  779     protected function getStorage() : array
  780     {
  781         return $this->_storage ?? [];
  782     }
  783 
  784     /**
  785      * @param string $type
  786      * @param string $property
  787      * @return FlexCollectionInterface
  788      */
  789     protected function getCollectionByProperty($type, $property)
  790     {
  791         $directory = $this->getRelatedDirectory($type);
  792         $collection = $directory->getCollection();
  793         $list = $this->getNestedProperty($property) ?: [];
  794 
  795         /** @var FlexCollection $collection */
  796         $collection = $collection->filter(function ($object) use ($list) { return \in_array($object->id, $list, true); });
  797 
  798         return $collection;
  799     }
  800 
  801     /**
  802      * @param string $type
  803      * @return FlexDirectory
  804      * @throws \RuntimeException
  805      */
  806     protected function getRelatedDirectory($type): FlexDirectory
  807     {
  808         /** @var Flex $flex */
  809         $flex = Grav::instance()['flex_objects'];
  810         $directory = $flex->getDirectory($type);
  811         if (!$directory) {
  812             throw new \RuntimeException(ucfirst($type). ' directory does not exist!');
  813         }
  814 
  815         return $directory;
  816     }
  817 
  818     /**
  819      * @param string $layout
  820      * @return Template|TemplateWrapper
  821      * @throws LoaderError
  822      * @throws SyntaxError
  823      */
  824     protected function getTemplate($layout)
  825     {
  826         $grav = Grav::instance();
  827 
  828         /** @var Twig $twig */
  829         $twig = $grav['twig'];
  830 
  831         try {
  832             return $twig->twig()->resolveTemplate(
  833                 [
  834                     "flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig",
  835                     "flex-objects/layouts/_default/object/{$layout}.html.twig"
  836                 ]
  837             );
  838         } catch (LoaderError $e) {
  839             /** @var Debugger $debugger */
  840             $debugger = Grav::instance()['debugger'];
  841             $debugger->addException($e);
  842 
  843             return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']);
  844         }
  845     }
  846 
  847     /**
  848      * Filter data coming to constructor or $this->update() request.
  849      *
  850      * NOTE: The incoming data can be an arbitrary array so do not assume anything from its content.
  851      *
  852      * @param array $elements
  853      */
  854     protected function filterElements(array &$elements): void
  855     {
  856         if (!empty($elements['storage_key'])) {
  857             $this->_storage['storage_key'] = trim($elements['storage_key']);
  858         }
  859         if (!empty($elements['storage_timestamp'])) {
  860             $this->_storage['storage_timestamp'] = (int)$elements['storage_timestamp'];
  861         }
  862 
  863         unset ($elements['storage_key'], $elements['storage_timestamp'], $elements['_post_entries_save']);
  864     }
  865 
  866     /**
  867      * This methods allows you to override form objects in child classes.
  868      *
  869      * @param string $name Form name
  870      * @param array|null $form Form fields
  871      * @return FlexFormInterface
  872      */
  873     protected function createFormObject(string $name, array $form = null)
  874     {
  875         return new FlexForm($name, $this, $form);
  876     }
  877 }