123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732 |
- <?php
- /**
- * @package Grav\Framework\Flex
- *
- * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Framework\Flex;
- use Doctrine\Common\Collections\Collection;
- use Doctrine\Common\Collections\Criteria;
- use Grav\Common\Debugger;
- use Grav\Common\Grav;
- use Grav\Common\Inflector;
- use Grav\Common\Twig\Twig;
- use Grav\Common\User\Interfaces\UserInterface;
- use Grav\Common\Utils;
- use Grav\Framework\Cache\CacheInterface;
- use Grav\Framework\ContentBlock\HtmlBlock;
- use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
- use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
- use Grav\Framework\Object\ObjectCollection;
- use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
- use Psr\SimpleCache\InvalidArgumentException;
- use RocketTheme\Toolbox\Event\Event;
- use Twig\Error\LoaderError;
- use Twig\Error\SyntaxError;
- use Twig\Template;
- use Twig\TemplateWrapper;
- use function array_filter;
- use function get_class;
- use function in_array;
- use function is_array;
- use function is_scalar;
- /**
- * Class FlexCollection
- * @package Grav\Framework\Flex
- * @template T of FlexObjectInterface
- * @extends ObjectCollection<string,T>
- * @implements FlexCollectionInterface<T>
- */
- class FlexCollection extends ObjectCollection implements FlexCollectionInterface
- {
- /** @var FlexDirectory */
- private $_flexDirectory;
- /** @var string */
- private $_keyField = 'storage_key';
- /**
- * Get list of cached methods.
- *
- * @return array Returns a list of methods with their caching information.
- */
- public static function getCachedMethods(): array
- {
- return [
- 'getTypePrefix' => true,
- 'getType' => true,
- 'getFlexDirectory' => true,
- 'hasFlexFeature' => true,
- 'getFlexFeatures' => true,
- 'getCacheKey' => true,
- 'getCacheChecksum' => false,
- 'getTimestamp' => true,
- 'hasProperty' => true,
- 'getProperty' => true,
- 'hasNestedProperty' => true,
- 'getNestedProperty' => true,
- 'orderBy' => true,
- 'render' => false,
- 'isAuthorized' => 'session',
- 'search' => true,
- 'sort' => true,
- 'getDistinctValues' => true
- ];
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::createFromArray()
- */
- public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
- {
- $instance = new static($entries, $directory);
- $instance->setKeyField($keyField);
- return $instance;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::__construct()
- */
- public function __construct(array $entries = [], FlexDirectory $directory = null)
- {
- // @phpstan-ignore-next-line
- if (get_class($this) === __CLASS__) {
- user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericCollection or your own class instead', E_USER_DEPRECATED);
- }
- parent::__construct($entries);
- if ($directory) {
- $this->setFlexDirectory($directory)->setKey($directory->getFlexType());
- }
- }
- /**
- * {@inheritdoc}
- * @see FlexCommonInterface::hasFlexFeature()
- */
- public function hasFlexFeature(string $name): bool
- {
- return in_array($name, $this->getFlexFeatures(), true);
- }
- /**
- * {@inheritdoc}
- * @see FlexCommonInterface::hasFlexFeature()
- */
- public function getFlexFeatures(): array
- {
- /** @var array $implements */
- $implements = class_implements($this);
- $list = [];
- foreach ($implements as $interface) {
- if ($pos = strrpos($interface, '\\')) {
- $interface = substr($interface, $pos+1);
- }
- $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface));
- }
- return $list;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::search()
- */
- public function search(string $search, $properties = null, array $options = null)
- {
- $directory = $this->getFlexDirectory();
- $properties = $directory->getSearchProperties($properties);
- $options = $directory->getSearchOptions($options);
- $matching = $this->call('search', [$search, $properties, $options]);
- $matching = array_filter($matching);
- if ($matching) {
- arsort($matching, SORT_NUMERIC);
- }
- /** @var string[] $array */
- $array = array_keys($matching);
- /** @phpstan-var static<T> */
- return $this->select($array);
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::sort()
- */
- public function sort(array $order)
- {
- $criteria = Criteria::create()->orderBy($order);
- /** @phpstan-var FlexCollectionInterface<T> $matching */
- $matching = $this->matching($criteria);
- return $matching;
- }
- /**
- * @param array $filters
- * @return static
- * @phpstan-return static<T>
- */
- public function filterBy(array $filters)
- {
- $expr = Criteria::expr();
- $criteria = Criteria::create();
- foreach ($filters as $key => $value) {
- $criteria->andWhere($expr->eq($key, $value));
- }
- /** @phpstan-var static<T> */
- return $this->matching($criteria);
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexType()
- */
- public function getFlexType(): string
- {
- return $this->_flexDirectory->getFlexType();
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getFlexDirectory(): FlexDirectory
- {
- return $this->_flexDirectory;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getTimestamp()
- */
- public function getTimestamp(): int
- {
- $timestamps = $this->getTimestamps();
- return $timestamps ? max($timestamps) : time();
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getCacheKey(): string
- {
- return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1((string)json_encode($this->call('getKey')));
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getCacheChecksum(): string
- {
- $list = [];
- /**
- * @var string $key
- * @var FlexObjectInterface $object
- */
- foreach ($this as $key => $object) {
- $list[$key] = $object->getCacheChecksum();
- }
- return sha1((string)json_encode($list));
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getTimestamps(): array
- {
- /** @var int[] $timestamps */
- $timestamps = $this->call('getTimestamp');
- return $timestamps;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getStorageKeys(): array
- {
- /** @var string[] $keys */
- $keys = $this->call('getStorageKey');
- return $keys;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getFlexDirectory()
- */
- public function getFlexKeys(): array
- {
- /** @var string[] $keys */
- $keys = $this->call('getFlexKey');
- return $keys;
- }
- /**
- * Get all the values in property.
- *
- * Supports either single scalar values or array of scalar values.
- *
- * @param string $property Object property to be used to make groups.
- * @param string|null $separator Separator, defaults to '.'
- * @return array
- */
- public function getDistinctValues(string $property, string $separator = null): array
- {
- $list = [];
- /** @var FlexObjectInterface $element */
- foreach ($this->getIterator() as $element) {
- $value = (array)$element->getNestedProperty($property, null, $separator);
- foreach ($value as $v) {
- if (is_scalar($v)) {
- $t = gettype($v) . (string)$v;
- $list[$t] = $v;
- }
- }
- }
- return array_values($list);
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::withKeyField()
- */
- public function withKeyField(string $keyField = null)
- {
- $keyField = $keyField ?: 'key';
- if ($keyField === $this->getKeyField()) {
- return $this;
- }
- $entries = [];
- foreach ($this as $key => $object) {
- // TODO: remove hardcoded logic
- if ($keyField === 'storage_key') {
- $entries[$object->getStorageKey()] = $object;
- } elseif ($keyField === 'flex_key') {
- $entries[$object->getFlexKey()] = $object;
- } elseif ($keyField === 'key') {
- $entries[$object->getKey()] = $object;
- }
- }
- return $this->createFrom($entries, $keyField);
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::getIndex()
- */
- public function getIndex()
- {
- /** @phpstan-var FlexIndexInterface<T> */
- return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
- }
- /**
- * @inheritdoc}
- * @see FlexCollectionInterface::getCollection()
- * @return $this
- */
- public function getCollection()
- {
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexCollectionInterface::render()
- */
- public function render(string $layout = null, array $context = [])
- {
- if (!$layout) {
- $config = $this->getTemplateConfig();
- $layout = $config['collection']['defaults']['layout'] ?? 'default';
- }
- $type = $this->getFlexType();
- $grav = Grav::instance();
- /** @var Debugger $debugger */
- $debugger = $grav['debugger'];
- $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
- $key = null;
- foreach ($context as $value) {
- if (!is_scalar($value)) {
- $key = false;
- break;
- }
- }
- if ($key !== false) {
- $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
- $cache = $this->getCache('render');
- } else {
- $cache = null;
- }
- try {
- $data = $cache && $key ? $cache->get($key) : null;
- $block = $data ? HtmlBlock::fromArray($data) : null;
- } catch (InvalidArgumentException $e) {
- $debugger->addException($e);
- $block = null;
- } catch (\InvalidArgumentException $e) {
- $debugger->addException($e);
- $block = null;
- }
- $checksum = $this->getCacheChecksum();
- if ($block && $checksum !== $block->getChecksum()) {
- $block = null;
- }
- if (!$block) {
- $block = HtmlBlock::create($key ?: null);
- $block->setChecksum($checksum);
- if (!$key) {
- $block->disableCache();
- }
- $event = new Event([
- 'type' => 'flex',
- 'directory' => $this->getFlexDirectory(),
- 'collection' => $this,
- 'layout' => &$layout,
- 'context' => &$context
- ]);
- $this->triggerEvent('onRender', $event);
- $output = $this->getTemplate($layout)->render(
- [
- 'grav' => $grav,
- 'config' => $grav['config'],
- 'block' => $block,
- 'directory' => $this->getFlexDirectory(),
- 'collection' => $this,
- 'layout' => $layout
- ] + $context
- );
- if ($debugger->enabled()) {
- $output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
- }
- $block->setContent($output);
- try {
- $cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
- } catch (InvalidArgumentException $e) {
- $debugger->addException($e);
- }
- }
- $debugger->stopTimer('flex-collection-' . $debugKey);
- return $block;
- }
- /**
- * @param FlexDirectory $type
- * @return $this
- */
- public function setFlexDirectory(FlexDirectory $type)
- {
- $this->_flexDirectory = $type;
- return $this;
- }
- /**
- * @param string $key
- * @return array
- */
- public function getMetaData($key): array
- {
- $object = $this->get($key);
- return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
- }
- /**
- * @param string|null $namespace
- * @return CacheInterface
- */
- public function getCache(string $namespace = null)
- {
- return $this->_flexDirectory->getCache($namespace);
- }
- /**
- * @return string
- */
- public function getKeyField(): string
- {
- return $this->_keyField;
- }
- /**
- * @param string $action
- * @param string|null $scope
- * @param UserInterface|null $user
- * @return static
- * @phpstan-return static<T>
- */
- public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
- {
- $list = $this->call('isAuthorized', [$action, $scope, $user]);
- $list = array_filter($list);
- /** @var string[] $keys */
- $keys = array_keys($list);
- /** @phpstan-var static<T> */
- return $this->select($keys);
- }
- /**
- * @param string $value
- * @param string $field
- * @return FlexObjectInterface|null
- * @phpstan-return T|null
- */
- public function find($value, $field = 'id')
- {
- if ($value) {
- foreach ($this as $element) {
- if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
- return $element;
- }
- }
- }
- return null;
- }
- /**
- * @return array
- */
- #[\ReturnTypeWillChange]
- public function jsonSerialize()
- {
- $elements = [];
- /**
- * @var string $key
- * @var array|FlexObject $object
- */
- foreach ($this->getElements() as $key => $object) {
- $elements[$key] = is_array($object) ? $object : $object->jsonSerialize();
- }
- return $elements;
- }
- /**
- * @return array
- */
- #[\ReturnTypeWillChange]
- public function __debugInfo()
- {
- return [
- 'type:private' => $this->getFlexType(),
- 'key:private' => $this->getKey(),
- 'objects_key:private' => $this->getKeyField(),
- 'objects:private' => $this->getElements()
- ];
- }
- /**
- * Creates a new instance from the specified elements.
- *
- * This method is provided for derived classes to specify how a new
- * instance should be created when constructor semantics have changed.
- *
- * @param array $elements Elements.
- * @param string|null $keyField
- * @return static
- * @phpstan-return static<T>
- * @throws \InvalidArgumentException
- */
- protected function createFrom(array $elements, $keyField = null)
- {
- $collection = new static($elements, $this->_flexDirectory);
- $collection->setKeyField($keyField ?: $this->_keyField);
- return $collection;
- }
- /**
- * @return string
- */
- protected function getTypePrefix(): string
- {
- return 'c.';
- }
- /**
- * @return array
- */
- protected function getTemplateConfig(): array
- {
- $config = $this->getFlexDirectory()->getConfig('site.templates', []);
- $defaults = array_replace($config['defaults'] ?? [], $config['collection']['defaults'] ?? []);
- $config['collection']['defaults'] = $defaults;
- return $config;
- }
- /**
- * @param string $layout
- * @return array
- */
- protected function getTemplatePaths(string $layout): array
- {
- $config = $this->getTemplateConfig();
- $type = $this->getFlexType();
- $defaults = $config['collection']['defaults'] ?? [];
- $ext = $defaults['ext'] ?? '.html.twig';
- $types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null)));
- $paths = $config['collection']['paths'] ?? [
- 'flex/{TYPE}/collection/{LAYOUT}{EXT}',
- 'flex-objects/layouts/{TYPE}/collection/{LAYOUT}{EXT}'
- ];
- $table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s'];
- $lookups = [];
- foreach ($paths as $path) {
- $path = Utils::simpleTemplate($path, $table);
- foreach ($types as $type) {
- $lookups[] = sprintf($path, $type, $layout, $ext);
- }
- }
- return array_unique($lookups);
- }
- /**
- * @param string $layout
- * @return Template|TemplateWrapper
- * @throws LoaderError
- * @throws SyntaxError
- */
- protected function getTemplate($layout)
- {
- $grav = Grav::instance();
- /** @var Twig $twig */
- $twig = $grav['twig'];
- try {
- return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
- } catch (LoaderError $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
- }
- }
- /**
- * @param string $type
- * @return FlexDirectory
- */
- protected function getRelatedDirectory($type): ?FlexDirectory
- {
- /** @var Flex $flex */
- $flex = Grav::instance()['flex'];
- return $flex->getDirectory($type);
- }
- /**
- * @param string|null $keyField
- * @return void
- */
- protected function setKeyField($keyField = null): void
- {
- $this->_keyField = $keyField ?? 'storage_key';
- }
- // DEPRECATED METHODS
- /**
- * @param bool $prefix
- * @return string
- * @deprecated 1.6 Use `->getFlexType()` instead.
- */
- public function getType($prefix = false)
- {
- user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
- $type = $prefix ? $this->getTypePrefix() : '';
- return $type . $this->getFlexType();
- }
- /**
- * @param string $name
- * @param object|null $event
- * @return $this
- * @deprecated 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait
- */
- public function triggerEvent(string $name, $event = null)
- {
- user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED);
- if (null === $event) {
- $event = new Event([
- 'type' => 'flex',
- 'directory' => $this->getFlexDirectory(),
- 'collection' => $this
- ]);
- }
- if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
- $name = 'onFlexCollection' . substr($name, 2);
- }
- $grav = Grav::instance();
- if ($event instanceof Event) {
- $grav->fireEvent($name, $event);
- } else {
- $grav->dispatchEvent($event);
- }
- return $this;
- }
- }
|