FlexCollection.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. <?php
  2. /**
  3. * @package Grav\Framework\Flex
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Flex;
  9. use Doctrine\Common\Collections\Collection;
  10. use Doctrine\Common\Collections\Criteria;
  11. use Grav\Common\Debugger;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Inflector;
  14. use Grav\Common\Twig\Twig;
  15. use Grav\Common\User\Interfaces\UserInterface;
  16. use Grav\Common\Utils;
  17. use Grav\Framework\Cache\CacheInterface;
  18. use Grav\Framework\ContentBlock\HtmlBlock;
  19. use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
  20. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  21. use Grav\Framework\Object\ObjectCollection;
  22. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  23. use Psr\SimpleCache\InvalidArgumentException;
  24. use RocketTheme\Toolbox\Event\Event;
  25. use Twig\Error\LoaderError;
  26. use Twig\Error\SyntaxError;
  27. use Twig\Template;
  28. use Twig\TemplateWrapper;
  29. use function array_filter;
  30. use function get_class;
  31. use function in_array;
  32. use function is_array;
  33. use function is_scalar;
  34. /**
  35. * Class FlexCollection
  36. * @package Grav\Framework\Flex
  37. * @template T of FlexObjectInterface
  38. * @extends ObjectCollection<string,T>
  39. * @implements FlexCollectionInterface<T>
  40. */
  41. class FlexCollection extends ObjectCollection implements FlexCollectionInterface
  42. {
  43. /** @var FlexDirectory */
  44. private $_flexDirectory;
  45. /** @var string */
  46. private $_keyField = 'storage_key';
  47. /**
  48. * Get list of cached methods.
  49. *
  50. * @return array Returns a list of methods with their caching information.
  51. */
  52. public static function getCachedMethods(): array
  53. {
  54. return [
  55. 'getTypePrefix' => true,
  56. 'getType' => true,
  57. 'getFlexDirectory' => true,
  58. 'hasFlexFeature' => true,
  59. 'getFlexFeatures' => true,
  60. 'getCacheKey' => true,
  61. 'getCacheChecksum' => false,
  62. 'getTimestamp' => true,
  63. 'hasProperty' => true,
  64. 'getProperty' => true,
  65. 'hasNestedProperty' => true,
  66. 'getNestedProperty' => true,
  67. 'orderBy' => true,
  68. 'render' => false,
  69. 'isAuthorized' => 'session',
  70. 'search' => true,
  71. 'sort' => true,
  72. 'getDistinctValues' => true
  73. ];
  74. }
  75. /**
  76. * {@inheritdoc}
  77. * @see FlexCollectionInterface::createFromArray()
  78. */
  79. public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
  80. {
  81. $instance = new static($entries, $directory);
  82. $instance->setKeyField($keyField);
  83. return $instance;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. * @see FlexCollectionInterface::__construct()
  88. */
  89. public function __construct(array $entries = [], FlexDirectory $directory = null)
  90. {
  91. // @phpstan-ignore-next-line
  92. if (get_class($this) === __CLASS__) {
  93. 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);
  94. }
  95. parent::__construct($entries);
  96. if ($directory) {
  97. $this->setFlexDirectory($directory)->setKey($directory->getFlexType());
  98. }
  99. }
  100. /**
  101. * {@inheritdoc}
  102. * @see FlexCommonInterface::hasFlexFeature()
  103. */
  104. public function hasFlexFeature(string $name): bool
  105. {
  106. return in_array($name, $this->getFlexFeatures(), true);
  107. }
  108. /**
  109. * {@inheritdoc}
  110. * @see FlexCommonInterface::hasFlexFeature()
  111. */
  112. public function getFlexFeatures(): array
  113. {
  114. /** @var array $implements */
  115. $implements = class_implements($this);
  116. $list = [];
  117. foreach ($implements as $interface) {
  118. if ($pos = strrpos($interface, '\\')) {
  119. $interface = substr($interface, $pos+1);
  120. }
  121. $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface));
  122. }
  123. return $list;
  124. }
  125. /**
  126. * {@inheritdoc}
  127. * @see FlexCollectionInterface::search()
  128. */
  129. public function search(string $search, $properties = null, array $options = null)
  130. {
  131. $directory = $this->getFlexDirectory();
  132. $properties = $directory->getSearchProperties($properties);
  133. $options = $directory->getSearchOptions($options);
  134. $matching = $this->call('search', [$search, $properties, $options]);
  135. $matching = array_filter($matching);
  136. if ($matching) {
  137. arsort($matching, SORT_NUMERIC);
  138. }
  139. /** @var string[] $array */
  140. $array = array_keys($matching);
  141. /** @phpstan-var static<T> */
  142. return $this->select($array);
  143. }
  144. /**
  145. * {@inheritdoc}
  146. * @see FlexCollectionInterface::sort()
  147. */
  148. public function sort(array $order)
  149. {
  150. $criteria = Criteria::create()->orderBy($order);
  151. /** @phpstan-var FlexCollectionInterface<T> $matching */
  152. $matching = $this->matching($criteria);
  153. return $matching;
  154. }
  155. /**
  156. * @param array $filters
  157. * @return static
  158. * @phpstan-return static<T>
  159. */
  160. public function filterBy(array $filters)
  161. {
  162. $expr = Criteria::expr();
  163. $criteria = Criteria::create();
  164. foreach ($filters as $key => $value) {
  165. $criteria->andWhere($expr->eq($key, $value));
  166. }
  167. /** @phpstan-var static<T> */
  168. return $this->matching($criteria);
  169. }
  170. /**
  171. * {@inheritdoc}
  172. * @see FlexCollectionInterface::getFlexType()
  173. */
  174. public function getFlexType(): string
  175. {
  176. return $this->_flexDirectory->getFlexType();
  177. }
  178. /**
  179. * {@inheritdoc}
  180. * @see FlexCollectionInterface::getFlexDirectory()
  181. */
  182. public function getFlexDirectory(): FlexDirectory
  183. {
  184. return $this->_flexDirectory;
  185. }
  186. /**
  187. * {@inheritdoc}
  188. * @see FlexCollectionInterface::getTimestamp()
  189. */
  190. public function getTimestamp(): int
  191. {
  192. $timestamps = $this->getTimestamps();
  193. return $timestamps ? max($timestamps) : time();
  194. }
  195. /**
  196. * {@inheritdoc}
  197. * @see FlexCollectionInterface::getFlexDirectory()
  198. */
  199. public function getCacheKey(): string
  200. {
  201. return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1((string)json_encode($this->call('getKey')));
  202. }
  203. /**
  204. * {@inheritdoc}
  205. * @see FlexCollectionInterface::getFlexDirectory()
  206. */
  207. public function getCacheChecksum(): string
  208. {
  209. $list = [];
  210. /**
  211. * @var string $key
  212. * @var FlexObjectInterface $object
  213. */
  214. foreach ($this as $key => $object) {
  215. $list[$key] = $object->getCacheChecksum();
  216. }
  217. return sha1((string)json_encode($list));
  218. }
  219. /**
  220. * {@inheritdoc}
  221. * @see FlexCollectionInterface::getFlexDirectory()
  222. */
  223. public function getTimestamps(): array
  224. {
  225. /** @var int[] $timestamps */
  226. $timestamps = $this->call('getTimestamp');
  227. return $timestamps;
  228. }
  229. /**
  230. * {@inheritdoc}
  231. * @see FlexCollectionInterface::getFlexDirectory()
  232. */
  233. public function getStorageKeys(): array
  234. {
  235. /** @var string[] $keys */
  236. $keys = $this->call('getStorageKey');
  237. return $keys;
  238. }
  239. /**
  240. * {@inheritdoc}
  241. * @see FlexCollectionInterface::getFlexDirectory()
  242. */
  243. public function getFlexKeys(): array
  244. {
  245. /** @var string[] $keys */
  246. $keys = $this->call('getFlexKey');
  247. return $keys;
  248. }
  249. /**
  250. * Get all the values in property.
  251. *
  252. * Supports either single scalar values or array of scalar values.
  253. *
  254. * @param string $property Object property to be used to make groups.
  255. * @param string|null $separator Separator, defaults to '.'
  256. * @return array
  257. */
  258. public function getDistinctValues(string $property, string $separator = null): array
  259. {
  260. $list = [];
  261. /** @var FlexObjectInterface $element */
  262. foreach ($this->getIterator() as $element) {
  263. $value = (array)$element->getNestedProperty($property, null, $separator);
  264. foreach ($value as $v) {
  265. if (is_scalar($v)) {
  266. $t = gettype($v) . (string)$v;
  267. $list[$t] = $v;
  268. }
  269. }
  270. }
  271. return array_values($list);
  272. }
  273. /**
  274. * {@inheritdoc}
  275. * @see FlexCollectionInterface::withKeyField()
  276. */
  277. public function withKeyField(string $keyField = null)
  278. {
  279. $keyField = $keyField ?: 'key';
  280. if ($keyField === $this->getKeyField()) {
  281. return $this;
  282. }
  283. $entries = [];
  284. foreach ($this as $key => $object) {
  285. // TODO: remove hardcoded logic
  286. if ($keyField === 'storage_key') {
  287. $entries[$object->getStorageKey()] = $object;
  288. } elseif ($keyField === 'flex_key') {
  289. $entries[$object->getFlexKey()] = $object;
  290. } elseif ($keyField === 'key') {
  291. $entries[$object->getKey()] = $object;
  292. }
  293. }
  294. return $this->createFrom($entries, $keyField);
  295. }
  296. /**
  297. * {@inheritdoc}
  298. * @see FlexCollectionInterface::getIndex()
  299. */
  300. public function getIndex()
  301. {
  302. /** @phpstan-var FlexIndexInterface<T> */
  303. return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
  304. }
  305. /**
  306. * @inheritdoc}
  307. * @see FlexCollectionInterface::getCollection()
  308. * @return $this
  309. */
  310. public function getCollection()
  311. {
  312. return $this;
  313. }
  314. /**
  315. * {@inheritdoc}
  316. * @see FlexCollectionInterface::render()
  317. */
  318. public function render(string $layout = null, array $context = [])
  319. {
  320. if (!$layout) {
  321. $config = $this->getTemplateConfig();
  322. $layout = $config['collection']['defaults']['layout'] ?? 'default';
  323. }
  324. $type = $this->getFlexType();
  325. $grav = Grav::instance();
  326. /** @var Debugger $debugger */
  327. $debugger = $grav['debugger'];
  328. $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
  329. $key = null;
  330. foreach ($context as $value) {
  331. if (!is_scalar($value)) {
  332. $key = false;
  333. break;
  334. }
  335. }
  336. if ($key !== false) {
  337. $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
  338. $cache = $this->getCache('render');
  339. } else {
  340. $cache = null;
  341. }
  342. try {
  343. $data = $cache && $key ? $cache->get($key) : null;
  344. $block = $data ? HtmlBlock::fromArray($data) : null;
  345. } catch (InvalidArgumentException $e) {
  346. $debugger->addException($e);
  347. $block = null;
  348. } catch (\InvalidArgumentException $e) {
  349. $debugger->addException($e);
  350. $block = null;
  351. }
  352. $checksum = $this->getCacheChecksum();
  353. if ($block && $checksum !== $block->getChecksum()) {
  354. $block = null;
  355. }
  356. if (!$block) {
  357. $block = HtmlBlock::create($key ?: null);
  358. $block->setChecksum($checksum);
  359. if (!$key) {
  360. $block->disableCache();
  361. }
  362. $event = new Event([
  363. 'type' => 'flex',
  364. 'directory' => $this->getFlexDirectory(),
  365. 'collection' => $this,
  366. 'layout' => &$layout,
  367. 'context' => &$context
  368. ]);
  369. $this->triggerEvent('onRender', $event);
  370. $output = $this->getTemplate($layout)->render(
  371. [
  372. 'grav' => $grav,
  373. 'config' => $grav['config'],
  374. 'block' => $block,
  375. 'directory' => $this->getFlexDirectory(),
  376. 'collection' => $this,
  377. 'layout' => $layout
  378. ] + $context
  379. );
  380. if ($debugger->enabled()) {
  381. $output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
  382. }
  383. $block->setContent($output);
  384. try {
  385. $cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
  386. } catch (InvalidArgumentException $e) {
  387. $debugger->addException($e);
  388. }
  389. }
  390. $debugger->stopTimer('flex-collection-' . $debugKey);
  391. return $block;
  392. }
  393. /**
  394. * @param FlexDirectory $type
  395. * @return $this
  396. */
  397. public function setFlexDirectory(FlexDirectory $type)
  398. {
  399. $this->_flexDirectory = $type;
  400. return $this;
  401. }
  402. /**
  403. * @param string $key
  404. * @return array
  405. */
  406. public function getMetaData($key): array
  407. {
  408. $object = $this->get($key);
  409. return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
  410. }
  411. /**
  412. * @param string|null $namespace
  413. * @return CacheInterface
  414. */
  415. public function getCache(string $namespace = null)
  416. {
  417. return $this->_flexDirectory->getCache($namespace);
  418. }
  419. /**
  420. * @return string
  421. */
  422. public function getKeyField(): string
  423. {
  424. return $this->_keyField;
  425. }
  426. /**
  427. * @param string $action
  428. * @param string|null $scope
  429. * @param UserInterface|null $user
  430. * @return static
  431. * @phpstan-return static<T>
  432. */
  433. public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
  434. {
  435. $list = $this->call('isAuthorized', [$action, $scope, $user]);
  436. $list = array_filter($list);
  437. /** @var string[] $keys */
  438. $keys = array_keys($list);
  439. /** @phpstan-var static<T> */
  440. return $this->select($keys);
  441. }
  442. /**
  443. * @param string $value
  444. * @param string $field
  445. * @return FlexObjectInterface|null
  446. * @phpstan-return T|null
  447. */
  448. public function find($value, $field = 'id')
  449. {
  450. if ($value) {
  451. foreach ($this as $element) {
  452. if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
  453. return $element;
  454. }
  455. }
  456. }
  457. return null;
  458. }
  459. /**
  460. * @return array
  461. */
  462. #[\ReturnTypeWillChange]
  463. public function jsonSerialize()
  464. {
  465. $elements = [];
  466. /**
  467. * @var string $key
  468. * @var array|FlexObject $object
  469. */
  470. foreach ($this->getElements() as $key => $object) {
  471. $elements[$key] = is_array($object) ? $object : $object->jsonSerialize();
  472. }
  473. return $elements;
  474. }
  475. /**
  476. * @return array
  477. */
  478. #[\ReturnTypeWillChange]
  479. public function __debugInfo()
  480. {
  481. return [
  482. 'type:private' => $this->getFlexType(),
  483. 'key:private' => $this->getKey(),
  484. 'objects_key:private' => $this->getKeyField(),
  485. 'objects:private' => $this->getElements()
  486. ];
  487. }
  488. /**
  489. * Creates a new instance from the specified elements.
  490. *
  491. * This method is provided for derived classes to specify how a new
  492. * instance should be created when constructor semantics have changed.
  493. *
  494. * @param array $elements Elements.
  495. * @param string|null $keyField
  496. * @return static
  497. * @phpstan-return static<T>
  498. * @throws \InvalidArgumentException
  499. */
  500. protected function createFrom(array $elements, $keyField = null)
  501. {
  502. $collection = new static($elements, $this->_flexDirectory);
  503. $collection->setKeyField($keyField ?: $this->_keyField);
  504. return $collection;
  505. }
  506. /**
  507. * @return string
  508. */
  509. protected function getTypePrefix(): string
  510. {
  511. return 'c.';
  512. }
  513. /**
  514. * @return array
  515. */
  516. protected function getTemplateConfig(): array
  517. {
  518. $config = $this->getFlexDirectory()->getConfig('site.templates', []);
  519. $defaults = array_replace($config['defaults'] ?? [], $config['collection']['defaults'] ?? []);
  520. $config['collection']['defaults'] = $defaults;
  521. return $config;
  522. }
  523. /**
  524. * @param string $layout
  525. * @return array
  526. */
  527. protected function getTemplatePaths(string $layout): array
  528. {
  529. $config = $this->getTemplateConfig();
  530. $type = $this->getFlexType();
  531. $defaults = $config['collection']['defaults'] ?? [];
  532. $ext = $defaults['ext'] ?? '.html.twig';
  533. $types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null)));
  534. $paths = $config['collection']['paths'] ?? [
  535. 'flex/{TYPE}/collection/{LAYOUT}{EXT}',
  536. 'flex-objects/layouts/{TYPE}/collection/{LAYOUT}{EXT}'
  537. ];
  538. $table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s'];
  539. $lookups = [];
  540. foreach ($paths as $path) {
  541. $path = Utils::simpleTemplate($path, $table);
  542. foreach ($types as $type) {
  543. $lookups[] = sprintf($path, $type, $layout, $ext);
  544. }
  545. }
  546. return array_unique($lookups);
  547. }
  548. /**
  549. * @param string $layout
  550. * @return Template|TemplateWrapper
  551. * @throws LoaderError
  552. * @throws SyntaxError
  553. */
  554. protected function getTemplate($layout)
  555. {
  556. $grav = Grav::instance();
  557. /** @var Twig $twig */
  558. $twig = $grav['twig'];
  559. try {
  560. return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
  561. } catch (LoaderError $e) {
  562. /** @var Debugger $debugger */
  563. $debugger = Grav::instance()['debugger'];
  564. $debugger->addException($e);
  565. return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
  566. }
  567. }
  568. /**
  569. * @param string $type
  570. * @return FlexDirectory
  571. */
  572. protected function getRelatedDirectory($type): ?FlexDirectory
  573. {
  574. /** @var Flex $flex */
  575. $flex = Grav::instance()['flex'];
  576. return $flex->getDirectory($type);
  577. }
  578. /**
  579. * @param string|null $keyField
  580. * @return void
  581. */
  582. protected function setKeyField($keyField = null): void
  583. {
  584. $this->_keyField = $keyField ?? 'storage_key';
  585. }
  586. // DEPRECATED METHODS
  587. /**
  588. * @param bool $prefix
  589. * @return string
  590. * @deprecated 1.6 Use `->getFlexType()` instead.
  591. */
  592. public function getType($prefix = false)
  593. {
  594. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  595. $type = $prefix ? $this->getTypePrefix() : '';
  596. return $type . $this->getFlexType();
  597. }
  598. /**
  599. * @param string $name
  600. * @param object|null $event
  601. * @return $this
  602. * @deprecated 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait
  603. */
  604. public function triggerEvent(string $name, $event = null)
  605. {
  606. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED);
  607. if (null === $event) {
  608. $event = new Event([
  609. 'type' => 'flex',
  610. 'directory' => $this->getFlexDirectory(),
  611. 'collection' => $this
  612. ]);
  613. }
  614. if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
  615. $name = 'onFlexCollection' . substr($name, 2);
  616. }
  617. $grav = Grav::instance();
  618. if ($event instanceof Event) {
  619. $grav->fireEvent($name, $event);
  620. } else {
  621. $grav->dispatchEvent($event);
  622. }
  623. return $this;
  624. }
  625. }