FlexCollection.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <?php
  2. /**
  3. * @package Grav\Framework\Flex
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 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\Twig\Twig;
  14. use Grav\Common\User\Interfaces\UserInterface;
  15. use Grav\Framework\Cache\CacheInterface;
  16. use Grav\Framework\ContentBlock\ContentBlockInterface;
  17. use Grav\Framework\ContentBlock\HtmlBlock;
  18. use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
  19. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  20. use Grav\Framework\Object\ObjectCollection;
  21. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  22. use Psr\SimpleCache\InvalidArgumentException;
  23. use RocketTheme\Toolbox\Event\Event;
  24. use Twig\Error\LoaderError;
  25. use Twig\Error\SyntaxError;
  26. use Twig\TemplateWrapper;
  27. /**
  28. * Class FlexCollection
  29. * @package Grav\Framework\Flex
  30. */
  31. class FlexCollection extends ObjectCollection implements FlexCollectionInterface
  32. {
  33. /** @var FlexDirectory */
  34. private $_flexDirectory;
  35. /** @var string */
  36. private $_keyField;
  37. /**
  38. * Get list of cached methods.
  39. *
  40. * @return array Returns a list of methods with their caching information.
  41. */
  42. public static function getCachedMethods(): array
  43. {
  44. return [
  45. 'getTypePrefix' => true,
  46. 'getType' => true,
  47. 'getFlexDirectory' => true,
  48. 'getCacheKey' => true,
  49. 'getCacheChecksum' => true,
  50. 'getTimestamp' => true,
  51. 'hasProperty' => true,
  52. 'getProperty' => true,
  53. 'hasNestedProperty' => true,
  54. 'getNestedProperty' => true,
  55. 'orderBy' => true,
  56. 'render' => false,
  57. 'isAuthorized' => 'session',
  58. 'search' => true,
  59. 'sort' => true,
  60. ];
  61. }
  62. /**
  63. * {@inheritdoc}
  64. * @see FlexCollectionInterface::createFromArray()
  65. */
  66. public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
  67. {
  68. $instance = new static($entries, $directory);
  69. $instance->setKeyField($keyField);
  70. return $instance;
  71. }
  72. /**
  73. * {@inheritdoc}
  74. * @see FlexCollectionInterface::__construct()
  75. */
  76. public function __construct(array $entries = [], FlexDirectory $directory = null)
  77. {
  78. parent::__construct($entries);
  79. if ($directory) {
  80. $this->setFlexDirectory($directory)->setKey($directory->getFlexType());
  81. }
  82. }
  83. /**
  84. * {@inheritdoc}
  85. * @see FlexCollectionInterface::search()
  86. */
  87. public function search(string $search, $properties = null, array $options = null)
  88. {
  89. $matching = $this->call('search', [$search, $properties, $options]);
  90. $matching = array_filter($matching);
  91. if ($matching) {
  92. uksort($matching, function ($a, $b) {
  93. return -($a <=> $b);
  94. });
  95. }
  96. return $this->select(array_keys($matching));
  97. }
  98. /**
  99. * {@inheritdoc}
  100. * @see FlexCollectionInterface::sort()
  101. */
  102. public function sort(array $order)
  103. {
  104. $criteria = Criteria::create()->orderBy($order);
  105. /** @var FlexCollectionInterface $matching */
  106. $matching = $this->matching($criteria);
  107. return $matching;
  108. }
  109. /**
  110. * @param array $filters
  111. * @return FlexCollectionInterface|Collection
  112. */
  113. public function filterBy(array $filters)
  114. {
  115. $expr = Criteria::expr();
  116. $criteria = Criteria::create();
  117. foreach ($filters as $key => $value) {
  118. $criteria->andWhere($expr->eq($key, $value));
  119. }
  120. return $this->matching($criteria);
  121. }
  122. /**
  123. * {@inheritdoc}
  124. * @see FlexCollectionInterface::getFlexType()
  125. */
  126. public function getFlexType(): string
  127. {
  128. return $this->_flexDirectory->getFlexType();
  129. }
  130. /**
  131. * {@inheritdoc}
  132. * @see FlexCollectionInterface::getFlexDirectory()
  133. */
  134. public function getFlexDirectory(): FlexDirectory
  135. {
  136. return $this->_flexDirectory;
  137. }
  138. /**
  139. * {@inheritdoc}
  140. * @see FlexCollectionInterface::getTimestamp()
  141. */
  142. public function getTimestamp(): int
  143. {
  144. $timestamps = $this->getTimestamps();
  145. return $timestamps ? max($timestamps) : time();
  146. }
  147. /**
  148. * {@inheritdoc}
  149. * @see FlexCollectionInterface::getFlexDirectory()
  150. */
  151. public function getCacheKey(): string
  152. {
  153. return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->call('getKey')));
  154. }
  155. /**
  156. * {@inheritdoc}
  157. * @see FlexCollectionInterface::getFlexDirectory()
  158. */
  159. public function getCacheChecksum(): string
  160. {
  161. return sha1(json_encode($this->getTimestamps()));
  162. }
  163. /**
  164. * {@inheritdoc}
  165. * @see FlexCollectionInterface::getFlexDirectory()
  166. */
  167. public function getTimestamps(): array
  168. {
  169. /** @var int[] $timestamps */
  170. $timestamps = $this->call('getTimestamp');
  171. return $timestamps;
  172. }
  173. /**
  174. * {@inheritdoc}
  175. * @see FlexCollectionInterface::getFlexDirectory()
  176. */
  177. public function getStorageKeys(): array
  178. {
  179. /** @var string[] $keys */
  180. $keys = $this->call('getStorageKey');
  181. return $keys;
  182. }
  183. /**
  184. * {@inheritdoc}
  185. * @see FlexCollectionInterface::getFlexDirectory()
  186. */
  187. public function getFlexKeys(): array
  188. {
  189. /** @var string[] $keys */
  190. $keys = $this->call('getFlexKey');
  191. return $keys;
  192. }
  193. /**
  194. * {@inheritdoc}
  195. * @see FlexCollectionInterface::withKeyField()
  196. */
  197. public function withKeyField(string $keyField = null)
  198. {
  199. $keyField = $keyField ?: 'key';
  200. if ($keyField === $this->getKeyField()) {
  201. return $this;
  202. }
  203. $entries = [];
  204. foreach ($this as $key => $object) {
  205. // TODO: remove hardcoded logic
  206. if ($keyField === 'storage_key') {
  207. $entries[$object->getStorageKey()] = $object;
  208. } elseif ($keyField === 'flex_key') {
  209. $entries[$object->getFlexKey()] = $object;
  210. } elseif ($keyField === 'key') {
  211. $entries[$object->getKey()] = $object;
  212. }
  213. }
  214. return $this->createFrom($entries, $keyField);
  215. }
  216. /**
  217. * {@inheritdoc}
  218. * @see FlexCollectionInterface::getIndex()
  219. */
  220. public function getIndex()
  221. {
  222. return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
  223. }
  224. /**
  225. * {@inheritdoc}
  226. * @see FlexCollectionInterface::render()
  227. */
  228. public function render(string $layout = null, array $context = [])
  229. {
  230. if (null === $layout) {
  231. $layout = 'default';
  232. }
  233. $type = $this->getFlexType();
  234. $grav = Grav::instance();
  235. /** @var Debugger $debugger */
  236. $debugger = $grav['debugger'];
  237. $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
  238. $cache = $key = null;
  239. foreach ($context as $value) {
  240. if (!\is_scalar($value)) {
  241. $key = false;
  242. }
  243. }
  244. if ($key !== false) {
  245. $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
  246. $cache = $this->getCache('render');
  247. }
  248. try {
  249. $data = $cache ? $cache->get($key) : null;
  250. $block = $data ? HtmlBlock::fromArray($data) : null;
  251. } catch (InvalidArgumentException $e) {
  252. $debugger->addException($e);
  253. $block = null;
  254. } catch (\InvalidArgumentException $e) {
  255. $debugger->addException($e);
  256. $block = null;
  257. }
  258. $checksum = $this->getCacheChecksum();
  259. if ($block && $checksum !== $block->getChecksum()) {
  260. $block = null;
  261. }
  262. if (!$block) {
  263. $block = HtmlBlock::create($key);
  264. $block->setChecksum($checksum);
  265. if ($key === false) {
  266. $block->disableCache();
  267. }
  268. $grav->fireEvent('onFlexCollectionRender', new Event([
  269. 'collection' => $this,
  270. 'layout' => &$layout,
  271. 'context' => &$context
  272. ]));
  273. $output = $this->getTemplate($layout)->render(
  274. ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'collection' => $this, 'layout' => $layout] + $context
  275. );
  276. if ($debugger->enabled()) {
  277. $output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
  278. }
  279. $block->setContent($output);
  280. try {
  281. $cache && $block->isCached() && $cache->set($key, $block->toArray());
  282. } catch (InvalidArgumentException $e) {
  283. $debugger->addException($e);
  284. }
  285. }
  286. $debugger->stopTimer('flex-collection-' . $debugKey);
  287. return $block;
  288. }
  289. /**
  290. * @param bool $prefix
  291. * @return string
  292. * @deprecated 1.6 Use `->getFlexType()` instead.
  293. */
  294. public function getType($prefix = false)
  295. {
  296. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  297. $type = $prefix ? $this->getTypePrefix() : '';
  298. return $type . $this->getFlexType();
  299. }
  300. /**
  301. * @param FlexDirectory $type
  302. * @return $this
  303. */
  304. public function setFlexDirectory(FlexDirectory $type)
  305. {
  306. $this->_flexDirectory = $type;
  307. return $this;
  308. }
  309. /**
  310. * @return array
  311. */
  312. public function getMetaData(string $key) : array
  313. {
  314. $object = $this->get($key);
  315. return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
  316. }
  317. /**
  318. * @param string|null $namespace
  319. * @return CacheInterface
  320. */
  321. public function getCache(string $namespace = null)
  322. {
  323. return $this->_flexDirectory->getCache($namespace);
  324. }
  325. /**
  326. * @return string
  327. */
  328. public function getKeyField(): string
  329. {
  330. return $this->_keyField ?? 'storage_key';
  331. }
  332. /**
  333. * @param string $action
  334. * @param string|null $scope
  335. * @param UserInterface|null $user
  336. * @return static
  337. */
  338. public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
  339. {
  340. $list = $this->call('isAuthorized', [$action, $scope, $user]);
  341. $list = \array_filter($list);
  342. return $this->select(array_keys($list));
  343. }
  344. /**
  345. * @param string $value
  346. * @param string $field
  347. * @return FlexObject|null
  348. */
  349. public function find($value, $field = 'id')
  350. {
  351. if ($value) foreach ($this as $element) {
  352. if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
  353. return $element;
  354. }
  355. }
  356. return null;
  357. }
  358. /**
  359. * @return array
  360. */
  361. public function jsonSerialize()
  362. {
  363. $elements = [];
  364. /**
  365. * @var string $key
  366. * @var array|FlexObject $object
  367. */
  368. foreach ($this->getElements() as $key => $object) {
  369. $elements[$key] = \is_array($object) ? $object : $object->jsonSerialize();
  370. }
  371. return $elements;
  372. }
  373. public function __debugInfo()
  374. {
  375. return [
  376. 'type:private' => $this->getFlexType(),
  377. 'key:private' => $this->getKey(),
  378. 'objects_key:private' => $this->getKeyField(),
  379. 'objects:private' => $this->getElements()
  380. ];
  381. }
  382. /**
  383. * Creates a new instance from the specified elements.
  384. *
  385. * This method is provided for derived classes to specify how a new
  386. * instance should be created when constructor semantics have changed.
  387. *
  388. * @param array $elements Elements.
  389. * @param string|null $keyField
  390. *
  391. * @return static
  392. * @throws \InvalidArgumentException
  393. */
  394. protected function createFrom(array $elements, $keyField = null)
  395. {
  396. $collection = new static($elements, $this->_flexDirectory);
  397. $collection->setKeyField($keyField ?: $this->_keyField);
  398. return $collection;
  399. }
  400. /**
  401. * @return string
  402. */
  403. protected function getTypePrefix(): string
  404. {
  405. return 'c.';
  406. }
  407. /**
  408. * @param string $layout
  409. * @return TemplateWrapper
  410. * @throws LoaderError
  411. * @throws SyntaxError
  412. */
  413. protected function getTemplate($layout)
  414. {
  415. $grav = Grav::instance();
  416. /** @var Twig $twig */
  417. $twig = $grav['twig'];
  418. try {
  419. return $twig->twig()->resolveTemplate(
  420. [
  421. "flex-objects/layouts/{$this->getFlexType()}/collection/{$layout}.html.twig",
  422. "flex-objects/layouts/_default/collection/{$layout}.html.twig"
  423. ]
  424. );
  425. } catch (LoaderError $e) {
  426. /** @var Debugger $debugger */
  427. $debugger = Grav::instance()['debugger'];
  428. $debugger->addException($e);
  429. return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']);
  430. }
  431. }
  432. /**
  433. * @param string $type
  434. * @return FlexDirectory
  435. */
  436. protected function getRelatedDirectory($type): ?FlexDirectory
  437. {
  438. /** @var Flex $flex */
  439. $flex = Grav::instance()['flex_objects'];
  440. return $flex->getDirectory($type);
  441. }
  442. protected function setKeyField($keyField = null): void
  443. {
  444. $this->_keyField = $keyField ?? 'storage_key';
  445. }
  446. }