FlexDirectory.php 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  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 Exception;
  10. use Grav\Common\Cache;
  11. use Grav\Common\Config\Config;
  12. use Grav\Common\Data\Blueprint;
  13. use Grav\Common\Debugger;
  14. use Grav\Common\Grav;
  15. use Grav\Common\Page\Interfaces\PageInterface;
  16. use Grav\Common\User\Interfaces\UserInterface;
  17. use Grav\Common\Utils;
  18. use Grav\Framework\Cache\Adapter\DoctrineCache;
  19. use Grav\Framework\Cache\Adapter\MemoryCache;
  20. use Grav\Framework\Cache\CacheInterface;
  21. use Grav\Framework\Filesystem\Filesystem;
  22. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  23. use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
  24. use Grav\Framework\Flex\Interfaces\FlexFormInterface;
  25. use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
  26. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  27. use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
  28. use Grav\Framework\Flex\Storage\SimpleStorage;
  29. use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
  30. use Psr\SimpleCache\InvalidArgumentException;
  31. use RocketTheme\Toolbox\File\YamlFile;
  32. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  33. use RuntimeException;
  34. use function call_user_func_array;
  35. use function count;
  36. use function is_array;
  37. use Grav\Common\Flex\Types\Generic\GenericObject;
  38. use Grav\Common\Flex\Types\Generic\GenericCollection;
  39. use Grav\Common\Flex\Types\Generic\GenericIndex;
  40. use function is_callable;
  41. /**
  42. * Class FlexDirectory
  43. * @package Grav\Framework\Flex
  44. */
  45. class FlexDirectory implements FlexDirectoryInterface
  46. {
  47. use FlexAuthorizeTrait;
  48. /** @var string */
  49. protected $type;
  50. /** @var string */
  51. protected $blueprint_file;
  52. /** @var Blueprint[] */
  53. protected $blueprints;
  54. /**
  55. * @var FlexIndexInterface[]
  56. * @phpstan-var FlexIndexInterface<FlexObjectInterface>[]
  57. */
  58. protected $indexes = [];
  59. /**
  60. * @var FlexCollectionInterface|null
  61. * @phpstan-var FlexCollectionInterface<FlexObjectInterface>|null
  62. */
  63. protected $collection;
  64. /** @var bool */
  65. protected $enabled;
  66. /** @var array */
  67. protected $defaults;
  68. /** @var Config */
  69. protected $config;
  70. /** @var FlexStorageInterface */
  71. protected $storage;
  72. /** @var CacheInterface[] */
  73. protected $cache;
  74. /** @var FlexObjectInterface[] */
  75. protected $objects;
  76. /** @var string */
  77. protected $objectClassName;
  78. /** @var string */
  79. protected $collectionClassName;
  80. /** @var string */
  81. protected $indexClassName;
  82. /** @var string|null */
  83. private $_authorize;
  84. /**
  85. * FlexDirectory constructor.
  86. * @param string $type
  87. * @param string $blueprint_file
  88. * @param array $defaults
  89. */
  90. public function __construct(string $type, string $blueprint_file, array $defaults = [])
  91. {
  92. $this->type = $type;
  93. $this->blueprints = [];
  94. $this->blueprint_file = $blueprint_file;
  95. $this->defaults = $defaults;
  96. $this->enabled = !empty($defaults['enabled']);
  97. $this->objects = [];
  98. }
  99. /**
  100. * @return bool
  101. */
  102. public function isListed(): bool
  103. {
  104. $grav = Grav::instance();
  105. /** @var Flex $flex */
  106. $flex = $grav['flex'];
  107. $directory = $flex->getDirectory($this->type);
  108. return null !== $directory;
  109. }
  110. /**
  111. * @return bool
  112. */
  113. public function isEnabled(): bool
  114. {
  115. return $this->enabled;
  116. }
  117. /**
  118. * @return string
  119. */
  120. public function getFlexType(): string
  121. {
  122. return $this->type;
  123. }
  124. /**
  125. * @return string
  126. */
  127. public function getTitle(): string
  128. {
  129. return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType()));
  130. }
  131. /**
  132. * @return string
  133. */
  134. public function getDescription(): string
  135. {
  136. return $this->getBlueprintInternal()->get('description', '');
  137. }
  138. /**
  139. * @param string|null $name
  140. * @param mixed $default
  141. * @return mixed
  142. */
  143. public function getConfig(string $name = null, $default = null)
  144. {
  145. if (null === $this->config) {
  146. $config = $this->getBlueprintInternal()->get('config', []);
  147. $config = is_array($config) ? array_replace_recursive($config, $this->defaults, $this->getDirectoryConfig($config['admin']['views']['configure']['form'] ?? $config['admin']['configure']['form'] ?? null)) : null;
  148. if (!is_array($config)) {
  149. throw new RuntimeException('Bad configuration');
  150. }
  151. $this->config = new Config($config);
  152. }
  153. return null === $name ? $this->config : $this->config->get($name, $default);
  154. }
  155. /**
  156. * @param string|string[]|null $properties
  157. * @return array
  158. */
  159. public function getSearchProperties($properties = null): array
  160. {
  161. if (null !== $properties) {
  162. return (array)$properties;
  163. }
  164. $properties = $this->getConfig('data.search.fields');
  165. if (!$properties) {
  166. $fields = $this->getConfig('admin.views.list.fields') ?? $this->getConfig('admin.list.fields', []);
  167. foreach ($fields as $property => $value) {
  168. if (!empty($value['link'])) {
  169. $properties[] = $property;
  170. }
  171. }
  172. }
  173. return $properties;
  174. }
  175. /**
  176. * @param array|null $options
  177. * @return array
  178. */
  179. public function getSearchOptions(array $options = null): array
  180. {
  181. if (empty($options['merge'])) {
  182. return $options ?? (array)$this->getConfig('data.search.options');
  183. }
  184. unset($options['merge']);
  185. return $options + (array)$this->getConfig('data.search.options');
  186. }
  187. /**
  188. * @param string|null $name
  189. * @param array $options
  190. * @return FlexFormInterface
  191. * @internal
  192. */
  193. public function getDirectoryForm(string $name = null, array $options = [])
  194. {
  195. $name = $name ?: $this->getConfig('admin.views.configure.form', '') ?: $this->getConfig('admin.configure.form', '');
  196. return new FlexDirectoryForm($name ?? '', $this, $options);
  197. }
  198. /**
  199. * @return Blueprint
  200. * @internal
  201. */
  202. public function getDirectoryBlueprint()
  203. {
  204. $name = 'configure';
  205. $type = $this->getBlueprint();
  206. $overrides = $type->get("blueprints/{$name}");
  207. $path = "blueprints://flex/shared/{$name}.yaml";
  208. $blueprint = new Blueprint($path);
  209. $blueprint->load();
  210. if (isset($overrides['fields'])) {
  211. $blueprint->embed('form/fields/tabs/fields', $overrides['fields']);
  212. }
  213. $blueprint->init();
  214. return $blueprint;
  215. }
  216. /**
  217. * @param string $name
  218. * @param array $data
  219. * @return void
  220. * @throws Exception
  221. * @internal
  222. */
  223. public function saveDirectoryConfig(string $name, array $data)
  224. {
  225. $grav = Grav::instance();
  226. /** @var UniformResourceLocator $locator */
  227. $locator = $grav['locator'];
  228. $filename = $this->getDirectoryConfigUri($name);
  229. if (file_exists($filename)) {
  230. $filename = $locator->findResource($filename, true);
  231. } else {
  232. $filesystem = Filesystem::getInstance();
  233. $dirname = $filesystem->dirname($filename);
  234. $basename = $filesystem->basename($filename);
  235. $dirname = $locator->findResource($dirname, true) ?: $locator->findResource($dirname, true, true);
  236. $filename = "{$dirname}/{$basename}";
  237. }
  238. $file = YamlFile::instance($filename);
  239. if (!empty($data)) {
  240. $file->save($data);
  241. } else {
  242. $file->delete();
  243. }
  244. }
  245. /**
  246. * @param string $name
  247. * @return array
  248. * @internal
  249. */
  250. public function loadDirectoryConfig(string $name): array
  251. {
  252. $grav = Grav::instance();
  253. /** @var UniformResourceLocator $locator */
  254. $locator = $grav['locator'];
  255. $uri = $this->getDirectoryConfigUri($name);
  256. // If configuration is found in main configuration, use it.
  257. if (str_starts_with($uri, 'config://')) {
  258. $path = str_replace('/', '.', substr($uri, 9, -5));
  259. return (array)$grav['config']->get($path);
  260. }
  261. // Load the configuration file.
  262. $filename = $locator->findResource($uri, true);
  263. if ($filename === false) {
  264. return [];
  265. }
  266. $file = YamlFile::instance($filename);
  267. return $file->content();
  268. }
  269. /**
  270. * @param string|null $name
  271. * @return string
  272. */
  273. public function getDirectoryConfigUri(string $name = null): string
  274. {
  275. $name = $name ?: $this->getFlexType();
  276. $blueprint = $this->getBlueprint();
  277. return $blueprint->get('blueprints/views/configure/file') ?? $blueprint->get('blueprints/configure/file') ?? "config://flex/{$name}.yaml";
  278. }
  279. /**
  280. * @param string|null $name
  281. * @return array
  282. */
  283. protected function getDirectoryConfig(string $name = null): array
  284. {
  285. $grav = Grav::instance();
  286. /** @var Config $config */
  287. $config = $grav['config'];
  288. $name = $name ?: $this->getFlexType();
  289. return $config->get("flex.{$name}", []);
  290. }
  291. /**
  292. * Returns a new uninitialized instance of blueprint.
  293. *
  294. * Always use $object->getBlueprint() or $object->getForm()->getBlueprint() instead.
  295. *
  296. * @param string $type
  297. * @param string $context
  298. * @return Blueprint
  299. */
  300. public function getBlueprint(string $type = '', string $context = '')
  301. {
  302. return clone $this->getBlueprintInternal($type, $context);
  303. }
  304. /**
  305. * @param string $view
  306. * @return string
  307. */
  308. public function getBlueprintFile(string $view = ''): string
  309. {
  310. $file = $this->blueprint_file;
  311. if ($view !== '') {
  312. $file = preg_replace('/\.yaml/', "/{$view}.yaml", $file);
  313. }
  314. return (string)$file;
  315. }
  316. /**
  317. * Get collection. In the site this will be filtered by the default filters (published etc).
  318. *
  319. * Use $directory->getIndex() if you want unfiltered collection.
  320. *
  321. * @param array|null $keys Array of keys.
  322. * @param string|null $keyField Field to be used as the key.
  323. * @return FlexCollectionInterface
  324. * @phpstan-return FlexCollectionInterface<FlexObjectInterface>
  325. */
  326. public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface
  327. {
  328. // Get all selected entries.
  329. $index = $this->getIndex($keys, $keyField);
  330. if (!Utils::isAdminPlugin()) {
  331. // If not in admin, filter the list by using default filters.
  332. $filters = (array)$this->getConfig('site.filter', []);
  333. foreach ($filters as $filter) {
  334. $index = $index->{$filter}();
  335. }
  336. }
  337. return $index;
  338. }
  339. /**
  340. * Get the full collection of all stored objects.
  341. *
  342. * Use $directory->getCollection() if you want a filtered collection.
  343. *
  344. * @param array|null $keys Array of keys.
  345. * @param string|null $keyField Field to be used as the key.
  346. * @return FlexIndexInterface
  347. * @phpstan-return FlexIndexInterface<FlexObjectInterface>
  348. */
  349. public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface
  350. {
  351. $keyField = $keyField ?? '';
  352. $index = $this->indexes[$keyField] ?? $this->loadIndex($keyField);
  353. $index = clone $index;
  354. if (null !== $keys) {
  355. /** @var FlexIndexInterface<FlexObjectInterface> $index */
  356. $index = $index->select($keys);
  357. }
  358. return $index->getIndex();
  359. }
  360. /**
  361. * Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object.
  362. *
  363. * Note: It is not safe to use the object without checking if the user can access it.
  364. *
  365. * @param string|null $key
  366. * @param string|null $keyField Field to be used as the key.
  367. * @return FlexObjectInterface|null
  368. */
  369. public function getObject($key = null, string $keyField = null): ?FlexObjectInterface
  370. {
  371. if (null === $key) {
  372. return $this->createObject([], '');
  373. }
  374. $keyField = $keyField ?? '';
  375. $index = $this->indexes[$keyField] ?? $this->loadIndex($keyField);
  376. return $index->get($key);
  377. }
  378. /**
  379. * @param string|null $namespace
  380. * @return CacheInterface
  381. */
  382. public function getCache(string $namespace = null)
  383. {
  384. $namespace = $namespace ?: 'index';
  385. $cache = $this->cache[$namespace] ?? null;
  386. if (null === $cache) {
  387. try {
  388. $grav = Grav::instance();
  389. /** @var Cache $gravCache */
  390. $gravCache = $grav['cache'];
  391. $config = $this->getConfig('object.cache.' . $namespace);
  392. if (empty($config['enabled'])) {
  393. $cache = new MemoryCache('flex-objects-' . $this->getFlexType());
  394. } else {
  395. $lifetime = $config['lifetime'] ?? 60;
  396. $key = $gravCache->getKey();
  397. if (Utils::isAdminPlugin()) {
  398. $key = substr($key, 0, -1);
  399. }
  400. $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $lifetime);
  401. }
  402. } catch (Exception $e) {
  403. /** @var Debugger $debugger */
  404. $debugger = Grav::instance()['debugger'];
  405. $debugger->addException($e);
  406. $cache = new MemoryCache('flex-objects-' . $this->getFlexType());
  407. }
  408. // Disable cache key validation.
  409. $cache->setValidation(false);
  410. $this->cache[$namespace] = $cache;
  411. }
  412. return $cache;
  413. }
  414. /**
  415. * @return $this
  416. */
  417. public function clearCache()
  418. {
  419. $grav = Grav::instance();
  420. /** @var Debugger $debugger */
  421. $debugger = $grav['debugger'];
  422. $debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug');
  423. /** @var UniformResourceLocator $locator */
  424. $locator = $grav['locator'];
  425. $locator->clearCache();
  426. $this->getCache('index')->clear();
  427. $this->getCache('object')->clear();
  428. $this->getCache('render')->clear();
  429. $this->indexes = [];
  430. $this->objects = [];
  431. return $this;
  432. }
  433. /**
  434. * @param string|null $key
  435. * @return string|null
  436. */
  437. public function getStorageFolder(string $key = null): ?string
  438. {
  439. return $this->getStorage()->getStoragePath($key);
  440. }
  441. /**
  442. * @param string|null $key
  443. * @return string|null
  444. */
  445. public function getMediaFolder(string $key = null): ?string
  446. {
  447. return $this->getStorage()->getMediaPath($key);
  448. }
  449. /**
  450. * @return FlexStorageInterface
  451. */
  452. public function getStorage(): FlexStorageInterface
  453. {
  454. if (null === $this->storage) {
  455. $this->storage = $this->createStorage();
  456. }
  457. return $this->storage;
  458. }
  459. /**
  460. * @param array $data
  461. * @param string $key
  462. * @param bool $validate
  463. * @return FlexObjectInterface
  464. */
  465. public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface
  466. {
  467. /** @phpstan-var class-string $className */
  468. $className = $this->objectClassName ?: $this->getObjectClass();
  469. if (!is_a($className, FlexObjectInterface::class, true)) {
  470. throw new \RuntimeException('Bad object class: ' . $className);
  471. }
  472. return new $className($data, $key, $this, $validate);
  473. }
  474. /**
  475. * @param array $entries
  476. * @param string|null $keyField
  477. * @return FlexCollectionInterface
  478. * @phpstan-return FlexCollectionInterface<FlexObjectInterface>
  479. */
  480. public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface
  481. {
  482. /** phpstan-var class-string $className */
  483. $className = $this->collectionClassName ?: $this->getCollectionClass();
  484. if (!is_a($className, FlexCollectionInterface::class, true)) {
  485. throw new \RuntimeException('Bad collection class: ' . $className);
  486. }
  487. return $className::createFromArray($entries, $this, $keyField);
  488. }
  489. /**
  490. * @param array $entries
  491. * @param string|null $keyField
  492. * @return FlexIndexInterface
  493. * @phpstan-return FlexIndexInterface<FlexObjectInterface>
  494. */
  495. public function createIndex(array $entries, string $keyField = null): FlexIndexInterface
  496. {
  497. /** @phpstan-var class-string $className */
  498. $className = $this->indexClassName ?: $this->getIndexClass();
  499. if (!is_a($className, FlexIndexInterface::class, true)) {
  500. throw new \RuntimeException('Bad index class: ' . $className);
  501. }
  502. return $className::createFromArray($entries, $this, $keyField);
  503. }
  504. /**
  505. * @return string
  506. */
  507. public function getObjectClass(): string
  508. {
  509. if (!$this->objectClassName) {
  510. $this->objectClassName = $this->getConfig('data.object', GenericObject::class);
  511. }
  512. return $this->objectClassName;
  513. }
  514. /**
  515. * @return string
  516. */
  517. public function getCollectionClass(): string
  518. {
  519. if (!$this->collectionClassName) {
  520. $this->collectionClassName = $this->getConfig('data.collection', GenericCollection::class);
  521. }
  522. return $this->collectionClassName;
  523. }
  524. /**
  525. * @return string
  526. */
  527. public function getIndexClass(): string
  528. {
  529. if (!$this->indexClassName) {
  530. $this->indexClassName = $this->getConfig('data.index', GenericIndex::class);
  531. }
  532. return $this->indexClassName;
  533. }
  534. /**
  535. * @param array $entries
  536. * @param string|null $keyField
  537. * @return FlexCollectionInterface
  538. * @phpstan-return FlexCollectionInterface<FlexObjectInterface>
  539. */
  540. public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface
  541. {
  542. return $this->createCollection($this->loadObjects($entries), $keyField);
  543. }
  544. /**
  545. * @param array $entries
  546. * @return FlexObjectInterface[]
  547. * @internal
  548. */
  549. public function loadObjects(array $entries): array
  550. {
  551. /** @var Debugger $debugger */
  552. $debugger = Grav::instance()['debugger'];
  553. $keys = [];
  554. $rows = [];
  555. $fetch = [];
  556. // Build lookup arrays with storage keys for the objects.
  557. foreach ($entries as $key => $value) {
  558. $k = $value['storage_key'] ?? '';
  559. if ($k === '') {
  560. continue;
  561. }
  562. $v = $this->objects[$k] ?? null;
  563. $keys[$k] = $key;
  564. $rows[$k] = $v;
  565. if (!$v) {
  566. $fetch[] = $k;
  567. }
  568. }
  569. // Attempt to fetch missing rows from the cache.
  570. if ($fetch) {
  571. $rows = (array)array_replace($rows, $this->loadCachedObjects($fetch));
  572. }
  573. // Read missing rows from the storage.
  574. $updated = [];
  575. $storage = $this->getStorage();
  576. $rows = $storage->readRows($rows, $updated);
  577. // Create objects from the rows.
  578. $isListed = $this->isListed();
  579. $list = [];
  580. foreach ($rows as $storageKey => $row) {
  581. $usedKey = $keys[$storageKey];
  582. if ($row instanceof FlexObjectInterface) {
  583. $object = $row;
  584. } else {
  585. if ($row === null) {
  586. $debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
  587. continue;
  588. }
  589. if (isset($row['__ERROR'])) {
  590. $message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__ERROR']);
  591. $debugger->addException(new RuntimeException($message));
  592. $debugger->addMessage($message, 'error');
  593. continue;
  594. }
  595. if (!isset($row['__META'])) {
  596. $row['__META'] = [
  597. 'storage_key' => $storageKey,
  598. 'storage_timestamp' => $entries[$usedKey]['storage_timestamp'] ?? 0,
  599. ];
  600. }
  601. $key = $row['__META']['key'] ?? $entries[$usedKey]['key'] ?? $usedKey;
  602. $object = $this->createObject($row, $key, false);
  603. $this->objects[$storageKey] = $object;
  604. if ($isListed) {
  605. // If unserialize works for the object, serialize the object to speed up the loading.
  606. $updated[$storageKey] = $object;
  607. }
  608. }
  609. $list[$usedKey] = $object;
  610. }
  611. // Store updated rows to the cache.
  612. if ($updated) {
  613. $cache = $this->getCache('object');
  614. if (!$cache instanceof MemoryCache) {
  615. ///** @var Debugger $debugger */
  616. //$debugger = Grav::instance()['debugger'];
  617. //$debugger->addMessage(sprintf('Flex: Caching %d %s', \count($entries), $this->type), 'debug');
  618. }
  619. try {
  620. $cache->setMultiple($updated);
  621. } catch (InvalidArgumentException $e) {
  622. $debugger->addException($e);
  623. // TODO: log about the issue.
  624. }
  625. }
  626. if ($fetch) {
  627. $debugger->stopTimer('flex-objects');
  628. }
  629. return $list;
  630. }
  631. protected function loadCachedObjects(array $fetch): array
  632. {
  633. if (!$fetch) {
  634. return [];
  635. }
  636. /** @var Debugger $debugger */
  637. $debugger = Grav::instance()['debugger'];
  638. $cache = $this->getCache('object');
  639. // Attempt to fetch missing rows from the cache.
  640. $fetched = [];
  641. try {
  642. $loading = count($fetch);
  643. $debugger->startTimer('flex-objects', sprintf('Flex: Loading %d %s', $loading, $this->type));
  644. $fetched = (array)$cache->getMultiple($fetch);
  645. if ($fetched) {
  646. $index = $this->loadIndex('storage_key');
  647. // Make sure cached objects are up to date: compare against index checksum/timestamp.
  648. /**
  649. * @var string $key
  650. * @var mixed $value
  651. */
  652. foreach ($fetched as $key => $value) {
  653. if ($value instanceof FlexObjectInterface) {
  654. $objectMeta = $value->getMetaData();
  655. } else {
  656. $objectMeta = $value['__META'] ?? [];
  657. }
  658. $indexMeta = $index->getMetaData($key);
  659. $indexChecksum = $indexMeta['checksum'] ?? $indexMeta['storage_timestamp'] ?? null;
  660. $objectChecksum = $objectMeta['checksum'] ?? $objectMeta['storage_timestamp'] ?? null;
  661. if ($indexChecksum !== $objectChecksum) {
  662. unset($fetched[$key]);
  663. }
  664. }
  665. }
  666. } catch (InvalidArgumentException $e) {
  667. $debugger->addException($e);
  668. }
  669. return $fetched;
  670. }
  671. /**
  672. * @return void
  673. */
  674. public function reloadIndex(): void
  675. {
  676. $this->getCache('index')->clear();
  677. $this->getIndex()::loadEntriesFromStorage($this->getStorage());
  678. $this->indexes = [];
  679. $this->objects = [];
  680. }
  681. /**
  682. * @param string $scope
  683. * @param string $action
  684. * @return string
  685. */
  686. public function getAuthorizeRule(string $scope, string $action): string
  687. {
  688. if (!$this->_authorize) {
  689. $config = $this->getConfig('admin.permissions');
  690. if ($config) {
  691. $this->_authorize = array_key_first($config) . '.%2$s';
  692. } else {
  693. $this->_authorize = '%1$s.flex-object.%2$s';
  694. }
  695. }
  696. return sprintf($this->_authorize, $scope, $action);
  697. }
  698. /**
  699. * @param string $type_view
  700. * @param string $context
  701. * @return Blueprint
  702. */
  703. protected function getBlueprintInternal(string $type_view = '', string $context = '')
  704. {
  705. if (!isset($this->blueprints[$type_view])) {
  706. if (!file_exists($this->blueprint_file)) {
  707. throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type));
  708. }
  709. $parts = explode('.', rtrim($type_view, '.'), 2);
  710. $type = array_shift($parts);
  711. $view = array_shift($parts) ?: '';
  712. $blueprint = new Blueprint($this->getBlueprintFile($view));
  713. $blueprint->addDynamicHandler('data', function (array &$field, $property, array &$call) {
  714. $this->dynamicDataField($field, $property, $call);
  715. });
  716. $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
  717. $this->dynamicFlexField($field, $property, $call);
  718. });
  719. $blueprint->addDynamicHandler('authorize', function (array &$field, $property, array &$call) {
  720. $this->dynamicAuthorizeField($field, $property, $call);
  721. });
  722. if ($context) {
  723. $blueprint->setContext($context);
  724. }
  725. $blueprint->load($type ?: null);
  726. if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) {
  727. $blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load();
  728. $blueprint->extend($blueprintBase, true);
  729. }
  730. $this->blueprints[$type_view] = $blueprint;
  731. }
  732. return $this->blueprints[$type_view];
  733. }
  734. /**
  735. * @param array $field
  736. * @param string $property
  737. * @param array $call
  738. * @return void
  739. */
  740. protected function dynamicDataField(array &$field, $property, array $call)
  741. {
  742. $params = $call['params'];
  743. if (is_array($params)) {
  744. $function = array_shift($params);
  745. } else {
  746. $function = $params;
  747. $params = [];
  748. }
  749. $object = $call['object'];
  750. if ($function === '\Grav\Common\Page\Pages::pageTypes') {
  751. $params = [$object instanceof PageInterface && $object->isModule() ? 'modular' : 'standard'];
  752. }
  753. $data = null;
  754. if (is_callable($function)) {
  755. $data = call_user_func_array($function, $params);
  756. }
  757. // If function returns a value,
  758. if (null !== $data) {
  759. if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
  760. // Combine field and @data-field together.
  761. $field[$property] += $data;
  762. } else {
  763. // Or create/replace field with @data-field.
  764. $field[$property] = $data;
  765. }
  766. }
  767. }
  768. /**
  769. * @param array $field
  770. * @param string $property
  771. * @param array $call
  772. * @return void
  773. */
  774. protected function dynamicFlexField(array &$field, $property, array $call): void
  775. {
  776. $params = (array)$call['params'];
  777. $object = $call['object'] ?? null;
  778. $method = array_shift($params);
  779. $not = false;
  780. if (str_starts_with($method, '!')) {
  781. $method = substr($method, 1);
  782. $not = true;
  783. } elseif (str_starts_with($method, 'not ')) {
  784. $method = substr($method, 4);
  785. $not = true;
  786. }
  787. $method = trim($method);
  788. if ($object && method_exists($object, $method)) {
  789. $value = $object->{$method}(...$params);
  790. if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
  791. $value = $this->mergeArrays($field[$property], $value);
  792. }
  793. $value = $not ? !$value : $value;
  794. if ($property === 'ignore' && $value) {
  795. Blueprint::addPropertyRecursive($field, 'validate', ['ignore' => true]);
  796. } else {
  797. $field[$property] = $value;
  798. }
  799. }
  800. }
  801. /**
  802. * @param array $field
  803. * @param string $property
  804. * @param array $call
  805. * @return void
  806. */
  807. protected function dynamicAuthorizeField(array &$field, $property, array $call): void
  808. {
  809. $params = (array)$call['params'];
  810. $object = $call['object'] ?? null;
  811. $permission = array_shift($params);
  812. $not = false;
  813. if (str_starts_with($permission, '!')) {
  814. $permission = substr($permission, 1);
  815. $not = true;
  816. } elseif (str_starts_with($permission, 'not ')) {
  817. $permission = substr($permission, 4);
  818. $not = true;
  819. }
  820. $permission = trim($permission);
  821. if ($object) {
  822. $value = $object->isAuthorized($permission) ?? false;
  823. $field[$property] = $not ? !$value : $value;
  824. }
  825. }
  826. /**
  827. * @param array $array1
  828. * @param array $array2
  829. * @return array
  830. */
  831. protected function mergeArrays(array $array1, array $array2): array
  832. {
  833. foreach ($array2 as $key => $value) {
  834. if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  835. $array1[$key] = $this->mergeArrays($array1[$key], $value);
  836. } else {
  837. $array1[$key] = $value;
  838. }
  839. }
  840. return $array1;
  841. }
  842. /**
  843. * @return FlexStorageInterface
  844. */
  845. protected function createStorage(): FlexStorageInterface
  846. {
  847. $this->collection = $this->createCollection([]);
  848. $storage = $this->getConfig('data.storage');
  849. if (!is_array($storage)) {
  850. $storage = ['options' => ['folder' => $storage]];
  851. }
  852. $className = $storage['class'] ?? SimpleStorage::class;
  853. $options = $storage['options'] ?? [];
  854. if (!is_a($className, FlexStorageInterface::class, true)) {
  855. throw new \RuntimeException('Bad storage class: ' . $className);
  856. }
  857. return new $className($options);
  858. }
  859. /**
  860. * @param string $keyField
  861. * @return FlexIndexInterface
  862. * @phpstan-return FlexIndexInterface<FlexObjectInterface>
  863. */
  864. protected function loadIndex(string $keyField): FlexIndexInterface
  865. {
  866. static $i = 0;
  867. $index = $this->indexes[$keyField] ?? null;
  868. if (null !== $index) {
  869. return $index;
  870. }
  871. $index = $this->indexes['storage_key'] ?? null;
  872. if (null === $index) {
  873. $i++;
  874. $j = $i;
  875. /** @var Debugger $debugger */
  876. $debugger = Grav::instance()['debugger'];
  877. $debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index");
  878. $storage = $this->getStorage();
  879. $cache = $this->getCache('index');
  880. try {
  881. $keys = $cache->get('__keys');
  882. } catch (InvalidArgumentException $e) {
  883. $debugger->addException($e);
  884. $keys = null;
  885. }
  886. if (!is_array($keys)) {
  887. /** @phpstan-var class-string $className */
  888. $className = $this->getIndexClass();
  889. $keys = $className::loadEntriesFromStorage($storage);
  890. if (!$cache instanceof MemoryCache) {
  891. $debugger->addMessage(
  892. sprintf('Flex: Caching %s index of %d objects', $this->type, count($keys)),
  893. 'debug'
  894. );
  895. }
  896. try {
  897. $cache->set('__keys', $keys);
  898. } catch (InvalidArgumentException $e) {
  899. $debugger->addException($e);
  900. // TODO: log about the issue.
  901. }
  902. }
  903. $ordering = $this->getConfig('data.ordering', []);
  904. // We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop.
  905. $this->indexes['storage_key'] = $index = $this->createIndex($keys, 'storage_key');
  906. if ($ordering) {
  907. /** @var FlexCollectionInterface<FlexObjectInterface> $collection */
  908. $collection = $this->indexes['storage_key']->orderBy($ordering);
  909. $this->indexes['storage_key'] = $index = $collection->getIndex();
  910. }
  911. $debugger->stopTimer('flex-keys-' . $this->type . $j);
  912. }
  913. if ($keyField !== 'storage_key') {
  914. $this->indexes[$keyField] = $index = $index->withKeyField($keyField ?: null);
  915. }
  916. return $index;
  917. }
  918. /**
  919. * @param string $action
  920. * @return string
  921. */
  922. protected function getAuthorizeAction(string $action): string
  923. {
  924. // Handle special action save, which can mean either update or create.
  925. if ($action === 'save') {
  926. $action = 'create';
  927. }
  928. return $action;
  929. }
  930. /**
  931. * @return UserInterface|null
  932. */
  933. protected function getActiveUser(): ?UserInterface
  934. {
  935. /** @var UserInterface|null $user */
  936. $user = Grav::instance()['user'] ?? null;
  937. return $user;
  938. }
  939. /**
  940. * @return string
  941. */
  942. protected function getAuthorizeScope(): string
  943. {
  944. return isset(Grav::instance()['admin']) ? 'admin' : 'site';
  945. }
  946. // DEPRECATED METHODS
  947. /**
  948. * @return string
  949. * @deprecated 1.6 Use ->getFlexType() method instead.
  950. */
  951. public function getType(): string
  952. {
  953. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  954. return $this->type;
  955. }
  956. /**
  957. * @param array $data
  958. * @param string|null $key
  959. * @return FlexObjectInterface
  960. * @deprecated 1.7 Use $object->update()->save() instead.
  961. */
  962. public function update(array $data, string $key = null): FlexObjectInterface
  963. {
  964. user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->update()->save() instead.', E_USER_DEPRECATED);
  965. $object = null !== $key ? $this->getIndex()->get($key): null;
  966. $storage = $this->getStorage();
  967. if (null === $object) {
  968. $object = $this->createObject($data, $key ?? '', true);
  969. $key = $object->getStorageKey();
  970. if ($key) {
  971. $storage->replaceRows([$key => $object->prepareStorage()]);
  972. } else {
  973. $storage->createRows([$object->prepareStorage()]);
  974. }
  975. } else {
  976. $oldKey = $object->getStorageKey();
  977. $object->update($data);
  978. $newKey = $object->getStorageKey();
  979. if ($oldKey !== $newKey) {
  980. if (method_exists($object, 'triggerEvent')) {
  981. $object->triggerEvent('move');
  982. }
  983. $storage->renameRow($oldKey, $newKey);
  984. // TODO: media support.
  985. }
  986. $object->save();
  987. }
  988. try {
  989. $this->clearCache();
  990. } catch (InvalidArgumentException $e) {
  991. /** @var Debugger $debugger */
  992. $debugger = Grav::instance()['debugger'];
  993. $debugger->addException($e);
  994. // Caching failed, but we can ignore that for now.
  995. }
  996. return $object;
  997. }
  998. /**
  999. * @param string $key
  1000. * @return FlexObjectInterface|null
  1001. * @deprecated 1.7 Use $object->delete() instead.
  1002. */
  1003. public function remove(string $key): ?FlexObjectInterface
  1004. {
  1005. user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->delete() instead.', E_USER_DEPRECATED);
  1006. $object = $this->getIndex()->get($key);
  1007. if (!$object) {
  1008. return null;
  1009. }
  1010. $object->delete();
  1011. return $object;
  1012. }
  1013. }