FlexDirectory.php 19 KB

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