FlexObject.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  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 ArrayAccess;
  10. use Exception;
  11. use Grav\Common\Data\Blueprint;
  12. use Grav\Common\Data\Data;
  13. use Grav\Common\Debugger;
  14. use Grav\Common\Grav;
  15. use Grav\Common\Inflector;
  16. use Grav\Common\Twig\Twig;
  17. use Grav\Common\User\Interfaces\UserInterface;
  18. use Grav\Common\Utils;
  19. use Grav\Framework\Cache\CacheInterface;
  20. use Grav\Framework\ContentBlock\HtmlBlock;
  21. use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
  22. use Grav\Framework\Flex\Interfaces\FlexFormInterface;
  23. use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
  24. use Grav\Framework\Flex\Traits\FlexRelatedDirectoryTrait;
  25. use Grav\Framework\Object\Access\NestedArrayAccessTrait;
  26. use Grav\Framework\Object\Access\NestedPropertyTrait;
  27. use Grav\Framework\Object\Access\OverloadedPropertyTrait;
  28. use Grav\Framework\Object\Base\ObjectTrait;
  29. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  30. use Grav\Framework\Object\Interfaces\ObjectInterface;
  31. use Grav\Framework\Object\Property\LazyPropertyTrait;
  32. use Psr\SimpleCache\InvalidArgumentException;
  33. use RocketTheme\Toolbox\Event\Event;
  34. use RuntimeException;
  35. use Twig\Error\LoaderError;
  36. use Twig\Error\SyntaxError;
  37. use Twig\Template;
  38. use Twig\TemplateWrapper;
  39. use function get_class;
  40. use function in_array;
  41. use function is_array;
  42. use function is_object;
  43. use function is_scalar;
  44. use function is_string;
  45. use function json_encode;
  46. /**
  47. * Class FlexObject
  48. * @package Grav\Framework\Flex
  49. */
  50. class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
  51. {
  52. use ObjectTrait;
  53. use LazyPropertyTrait {
  54. LazyPropertyTrait::__construct as private objectConstruct;
  55. }
  56. use NestedPropertyTrait;
  57. use OverloadedPropertyTrait;
  58. use NestedArrayAccessTrait;
  59. use FlexAuthorizeTrait;
  60. use FlexRelatedDirectoryTrait;
  61. /** @var FlexDirectory */
  62. private $_flexDirectory;
  63. /** @var FlexFormInterface[] */
  64. private $_forms = [];
  65. /** @var Blueprint[] */
  66. private $_blueprint = [];
  67. /** @var array|null */
  68. private $_meta;
  69. /** @var array|null */
  70. protected $_original;
  71. /** @var string|null */
  72. protected $storage_key;
  73. /** @var int|null */
  74. protected $storage_timestamp;
  75. /**
  76. * @return array
  77. */
  78. public static function getCachedMethods(): array
  79. {
  80. return [
  81. 'getTypePrefix' => true,
  82. 'getType' => true,
  83. 'getFlexType' => true,
  84. 'getFlexDirectory' => true,
  85. 'hasFlexFeature' => true,
  86. 'getFlexFeatures' => true,
  87. 'getCacheKey' => true,
  88. 'getCacheChecksum' => false,
  89. 'getTimestamp' => true,
  90. 'value' => true,
  91. 'exists' => true,
  92. 'hasProperty' => true,
  93. 'getProperty' => true,
  94. // FlexAclTrait
  95. 'isAuthorized' => 'session',
  96. ];
  97. }
  98. /**
  99. * @param array $elements
  100. * @param array $storage
  101. * @param FlexDirectory $directory
  102. * @param bool $validate
  103. * @return static
  104. */
  105. public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false)
  106. {
  107. $instance = new static($elements, $storage['key'], $directory, $validate);
  108. $instance->setMetaData($storage);
  109. return $instance;
  110. }
  111. /**
  112. * {@inheritdoc}
  113. * @see FlexObjectInterface::__construct()
  114. */
  115. public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
  116. {
  117. if (get_class($this) === __CLASS__) {
  118. user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericObject or your own class instead', E_USER_DEPRECATED);
  119. }
  120. $this->_flexDirectory = $directory;
  121. if (isset($elements['__META'])) {
  122. $this->setMetaData($elements['__META']);
  123. unset($elements['__META']);
  124. }
  125. if ($validate) {
  126. $blueprint = $this->getFlexDirectory()->getBlueprint();
  127. $blueprint->validate($elements, ['xss_check' => false]);
  128. $elements = $blueprint->filter($elements, true, true);
  129. }
  130. $this->filterElements($elements);
  131. $this->objectConstruct($elements, $key);
  132. }
  133. /**
  134. * {@inheritdoc}
  135. * @see FlexCommonInterface::hasFlexFeature()
  136. */
  137. public function hasFlexFeature(string $name): bool
  138. {
  139. return in_array($name, $this->getFlexFeatures(), true);
  140. }
  141. /**
  142. * {@inheritdoc}
  143. * @see FlexCommonInterface::hasFlexFeature()
  144. */
  145. public function getFlexFeatures(): array
  146. {
  147. /** @var array $implements */
  148. $implements = class_implements($this);
  149. $list = [];
  150. foreach ($implements as $interface) {
  151. if ($pos = strrpos($interface, '\\')) {
  152. $interface = substr($interface, $pos+1);
  153. }
  154. $list[] = Inflector::hyphenize(str_replace('Interface', '', $interface));
  155. }
  156. return $list;
  157. }
  158. /**
  159. * {@inheritdoc}
  160. * @see FlexObjectInterface::getFlexType()
  161. */
  162. public function getFlexType(): string
  163. {
  164. return $this->_flexDirectory->getFlexType();
  165. }
  166. /**
  167. * {@inheritdoc}
  168. * @see FlexObjectInterface::getFlexDirectory()
  169. */
  170. public function getFlexDirectory(): FlexDirectory
  171. {
  172. return $this->_flexDirectory;
  173. }
  174. /**
  175. * Refresh object from the storage.
  176. *
  177. * @param bool $keepMissing
  178. * @return bool True if the object was refreshed
  179. */
  180. public function refresh(bool $keepMissing = false): bool
  181. {
  182. $key = $this->getStorageKey();
  183. if ('' === $key) {
  184. return false;
  185. }
  186. $storage = $this->getFlexDirectory()->getStorage();
  187. $meta = $storage->getMetaData([$key])[$key] ?? null;
  188. $newChecksum = $meta['checksum'] ?? $meta['storage_timestamp'] ?? null;
  189. $curChecksum = $this->_meta['checksum'] ?? $this->_meta['storage_timestamp'] ?? null;
  190. // Check if object is up to date with the storage.
  191. if (null === $newChecksum || $newChecksum === $curChecksum) {
  192. return false;
  193. }
  194. // Get current elements (if requested).
  195. $current = $keepMissing ? $this->getElements() : [];
  196. // Get elements from the filesystem.
  197. $elements = $storage->readRows([$key => null])[$key] ?? null;
  198. if (null !== $elements) {
  199. $meta = $elements['__META'] ?? $meta;
  200. unset($elements['__META']);
  201. $this->filterElements($elements);
  202. $newKey = $meta['key'] ?? $this->getKey();
  203. if ($meta) {
  204. $this->setMetaData($meta);
  205. }
  206. $this->objectConstruct($elements, $newKey);
  207. if ($current) {
  208. // Inject back elements which are missing in the filesystem.
  209. $data = $this->getBlueprint()->flattenData($current);
  210. foreach ($data as $property => $value) {
  211. if (strpos($property, '.') === false) {
  212. $this->defProperty($property, $value);
  213. } else {
  214. $this->defNestedProperty($property, $value);
  215. }
  216. }
  217. }
  218. /** @var Debugger $debugger */
  219. $debugger = Grav::instance()['debugger'];
  220. $debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug');
  221. }
  222. return true;
  223. }
  224. /**
  225. * {@inheritdoc}
  226. * @see FlexObjectInterface::getTimestamp()
  227. */
  228. public function getTimestamp(): int
  229. {
  230. return $this->_meta['storage_timestamp'] ?? 0;
  231. }
  232. /**
  233. * {@inheritdoc}
  234. * @see FlexObjectInterface::getCacheKey()
  235. */
  236. public function getCacheKey(): string
  237. {
  238. return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() : '';
  239. }
  240. /**
  241. * {@inheritdoc}
  242. * @see FlexObjectInterface::getCacheChecksum()
  243. */
  244. public function getCacheChecksum(): string
  245. {
  246. return (string)($this->_meta['checksum'] ?? $this->getTimestamp());
  247. }
  248. /**
  249. * {@inheritdoc}
  250. * @see FlexObjectInterface::search()
  251. */
  252. public function search(string $search, $properties = null, array $options = null): float
  253. {
  254. $directory = $this->getFlexDirectory();
  255. $properties = $directory->getSearchProperties($properties);
  256. $options = $directory->getSearchOptions($options);
  257. $weight = 0;
  258. foreach ($properties as $property) {
  259. if (strpos($property, '.')) {
  260. $weight += $this->searchNestedProperty($property, $search, $options);
  261. } else {
  262. $weight += $this->searchProperty($property, $search, $options);
  263. }
  264. }
  265. return $weight > 0 ? min($weight, 1) : 0;
  266. }
  267. /**
  268. * {@inheritdoc}
  269. * @see ObjectInterface::getFlexKey()
  270. */
  271. public function getKey()
  272. {
  273. return (string)$this->_key;
  274. }
  275. /**
  276. * {@inheritdoc}
  277. * @see FlexObjectInterface::getFlexKey()
  278. */
  279. public function getFlexKey(): string
  280. {
  281. $key = $this->_meta['flex_key'] ?? null;
  282. if (!$key && $key = $this->getStorageKey()) {
  283. $key = $this->_flexDirectory->getFlexType() . '.obj:' . $key;
  284. }
  285. return (string)$key;
  286. }
  287. /**
  288. * {@inheritdoc}
  289. * @see FlexObjectInterface::getStorageKey()
  290. */
  291. public function getStorageKey(): string
  292. {
  293. return (string)($this->storage_key ?? $this->_meta['storage_key'] ?? null);
  294. }
  295. /**
  296. * {@inheritdoc}
  297. * @see FlexObjectInterface::getMetaData()
  298. */
  299. public function getMetaData(): array
  300. {
  301. return $this->_meta ?? [];
  302. }
  303. /**
  304. * {@inheritdoc}
  305. * @see FlexObjectInterface::exists()
  306. */
  307. public function exists(): bool
  308. {
  309. $key = $this->getStorageKey();
  310. return $key && $this->getFlexDirectory()->getStorage()->hasKey($key);
  311. }
  312. /**
  313. * @param string $property
  314. * @param string $search
  315. * @param array|null $options
  316. * @return float
  317. */
  318. public function searchProperty(string $property, string $search, array $options = null): float
  319. {
  320. $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
  321. $value = $this->getProperty($property);
  322. return $this->searchValue($property, $value, $search, $options);
  323. }
  324. /**
  325. * @param string $property
  326. * @param string $search
  327. * @param array|null $options
  328. * @return float
  329. */
  330. public function searchNestedProperty(string $property, string $search, array $options = null): float
  331. {
  332. $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
  333. if ($property === 'key') {
  334. $value = $this->getKey();
  335. } else {
  336. $value = $this->getNestedProperty($property);
  337. }
  338. return $this->searchValue($property, $value, $search, $options);
  339. }
  340. /**
  341. * @param string $name
  342. * @param mixed $value
  343. * @param string $search
  344. * @param array|null $options
  345. * @return float
  346. */
  347. protected function searchValue(string $name, $value, string $search, array $options = null): float
  348. {
  349. $options = $options ?? [];
  350. // Ignore empty search strings.
  351. $search = trim($search);
  352. if ($search === '') {
  353. return 0;
  354. }
  355. // Search only non-empty string values.
  356. if (!is_string($value) || $value === '') {
  357. return 0;
  358. }
  359. $caseSensitive = $options['case_sensitive'] ?? false;
  360. $tested = false;
  361. if (($tested |= !empty($options['same_as']))) {
  362. if ($caseSensitive) {
  363. if ($value === $search) {
  364. return (float)$options['same_as'];
  365. }
  366. } elseif (mb_strtolower($value) === mb_strtolower($search)) {
  367. return (float)$options['same_as'];
  368. }
  369. }
  370. if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $caseSensitive)) {
  371. return (float)$options['starts_with'];
  372. }
  373. if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $caseSensitive)) {
  374. return (float)$options['ends_with'];
  375. }
  376. if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $caseSensitive)) {
  377. return (float)($options['contains'] ?? 1);
  378. }
  379. return 0;
  380. }
  381. /**
  382. * Get original data before update
  383. *
  384. * @return array
  385. */
  386. public function getOriginalData(): array
  387. {
  388. return $this->_original ?? [];
  389. }
  390. /**
  391. * Get diff array from the object.
  392. *
  393. * @return array
  394. */
  395. public function getDiff(): array
  396. {
  397. $blueprint = $this->getBlueprint();
  398. $flattenOriginal = $blueprint->flattenData($this->getOriginalData());
  399. $flattenElements = $blueprint->flattenData($this->getElements());
  400. $removedElements = array_diff_key($flattenOriginal, $flattenElements);
  401. $diff = [];
  402. // Include all added or changed keys.
  403. foreach ($flattenElements as $key => $value) {
  404. $orig = $flattenOriginal[$key] ?? null;
  405. if ($orig !== $value) {
  406. $diff[$key] = ['old' => $orig, 'new' => $value];
  407. }
  408. }
  409. // Include all removed keys.
  410. foreach ($removedElements as $key => $value) {
  411. $diff[$key] = ['old' => $value, 'new' => null];
  412. }
  413. return $diff;
  414. }
  415. /**
  416. * Get any changes from the object.
  417. *
  418. * @return array
  419. */
  420. public function getChanges(): array
  421. {
  422. $diff = $this->getDiff();
  423. $data = new Data();
  424. foreach ($diff as $key => $change) {
  425. $data->set($key, $change['new']);
  426. }
  427. return $data->toArray();
  428. }
  429. /**
  430. * @return string
  431. */
  432. protected function getTypePrefix(): string
  433. {
  434. return 'o.';
  435. }
  436. /**
  437. * Alias of getBlueprint()
  438. *
  439. * @return Blueprint
  440. * @deprecated 1.6 Admin compatibility
  441. */
  442. public function blueprints()
  443. {
  444. return $this->getBlueprint();
  445. }
  446. /**
  447. * @param string|null $namespace
  448. * @return CacheInterface
  449. */
  450. public function getCache(string $namespace = null)
  451. {
  452. return $this->_flexDirectory->getCache($namespace);
  453. }
  454. /**
  455. * @param string|null $key
  456. * @return $this
  457. */
  458. public function setStorageKey($key = null)
  459. {
  460. $this->storage_key = $key ?? '';
  461. return $this;
  462. }
  463. /**
  464. * @param int $timestamp
  465. * @return $this
  466. */
  467. public function setTimestamp($timestamp = null)
  468. {
  469. $this->storage_timestamp = $timestamp ?? time();
  470. return $this;
  471. }
  472. /**
  473. * {@inheritdoc}
  474. * @see FlexObjectInterface::render()
  475. */
  476. public function render(string $layout = null, array $context = [])
  477. {
  478. if (!$layout) {
  479. $config = $this->getTemplateConfig();
  480. $layout = $config['object']['defaults']['layout'] ?? 'default';
  481. }
  482. $type = $this->getFlexType();
  483. $grav = Grav::instance();
  484. /** @var Debugger $debugger */
  485. $debugger = $grav['debugger'];
  486. $debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')');
  487. $key = $this->getCacheKey();
  488. // Disable caching if context isn't all scalars.
  489. if ($key) {
  490. foreach ($context as $value) {
  491. if (!is_scalar($value)) {
  492. $key = '';
  493. break;
  494. }
  495. }
  496. }
  497. if ($key) {
  498. // Create a new key which includes layout and context.
  499. $key = md5($key . '.' . $layout . json_encode($context));
  500. $cache = $this->getCache('render');
  501. } else {
  502. $cache = null;
  503. }
  504. try {
  505. $data = $cache ? $cache->get($key) : null;
  506. $block = $data ? HtmlBlock::fromArray($data) : null;
  507. } catch (InvalidArgumentException $e) {
  508. $debugger->addException($e);
  509. $block = null;
  510. } catch (\InvalidArgumentException $e) {
  511. $debugger->addException($e);
  512. $block = null;
  513. }
  514. $checksum = $this->getCacheChecksum();
  515. if ($block && $checksum !== $block->getChecksum()) {
  516. $block = null;
  517. }
  518. if (!$block) {
  519. $block = HtmlBlock::create($key ?: null);
  520. $block->setChecksum($checksum);
  521. if (!$cache) {
  522. $block->disableCache();
  523. }
  524. $event = new Event([
  525. 'type' => 'flex',
  526. 'directory' => $this->getFlexDirectory(),
  527. 'object' => $this,
  528. 'layout' => &$layout,
  529. 'context' => &$context
  530. ]);
  531. $this->triggerEvent('onRender', $event);
  532. $output = $this->getTemplate($layout)->render(
  533. [
  534. 'grav' => $grav,
  535. 'config' => $grav['config'],
  536. 'block' => $block,
  537. 'directory' => $this->getFlexDirectory(),
  538. 'object' => $this,
  539. 'layout' => $layout
  540. ] + $context
  541. );
  542. if ($debugger->enabled()) {
  543. $name = $this->getKey() . ' (' . $type . ')';
  544. $output = "\n<!–– START {$name} object ––>\n{$output}\n<!–– END {$name} object ––>\n";
  545. }
  546. $block->setContent($output);
  547. try {
  548. $cache && $block->isCached() && $cache->set($key, $block->toArray());
  549. } catch (InvalidArgumentException $e) {
  550. $debugger->addException($e);
  551. }
  552. }
  553. $debugger->stopTimer('flex-object-' . $debugKey);
  554. return $block;
  555. }
  556. /**
  557. * @return array
  558. */
  559. #[\ReturnTypeWillChange]
  560. public function jsonSerialize()
  561. {
  562. return $this->getElements();
  563. }
  564. /**
  565. * {@inheritdoc}
  566. * @see FlexObjectInterface::prepareStorage()
  567. */
  568. public function prepareStorage(): array
  569. {
  570. return ['__META' => $this->getMetaData()] + $this->getElements();
  571. }
  572. /**
  573. * {@inheritdoc}
  574. * @see FlexObjectInterface::update()
  575. */
  576. public function update(array $data, array $files = [])
  577. {
  578. if ($data) {
  579. // Get currently stored data.
  580. $elements = $this->getElements();
  581. // Store original version of the object.
  582. if ($this->_original === null) {
  583. $this->_original = $elements;
  584. }
  585. $blueprint = $this->getBlueprint();
  586. // Process updated data through the object filters.
  587. $this->filterElements($data);
  588. // Merge existing object to the test data to be validated.
  589. $test = $blueprint->mergeData($elements, $data);
  590. // Validate and filter elements and throw an error if any issues were found.
  591. $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()], ['xss_check' => false]);
  592. $data = $blueprint->filter($data, true, true);
  593. // Finally update the object.
  594. $flattenData = $blueprint->flattenData($data);
  595. foreach ($flattenData as $key => $value) {
  596. if ($value === null) {
  597. $this->unsetNestedProperty($key);
  598. } else {
  599. $this->setNestedProperty($key, $value);
  600. }
  601. }
  602. }
  603. if ($files && method_exists($this, 'setUpdatedMedia')) {
  604. $this->setUpdatedMedia($files);
  605. }
  606. return $this;
  607. }
  608. /**
  609. * {@inheritdoc}
  610. * @see FlexObjectInterface::create()
  611. */
  612. public function create(string $key = null)
  613. {
  614. if ($key) {
  615. $this->setStorageKey($key);
  616. }
  617. if ($this->exists()) {
  618. throw new RuntimeException('Cannot create new object (Already exists)');
  619. }
  620. return $this->save();
  621. }
  622. /**
  623. * @param string|null $key
  624. * @return FlexObject|FlexObjectInterface
  625. */
  626. public function createCopy(string $key = null)
  627. {
  628. $this->markAsCopy();
  629. return $this->create($key);
  630. }
  631. /**
  632. * @param UserInterface|null $user
  633. */
  634. public function check(UserInterface $user = null): void
  635. {
  636. // If user has been provided, check if the user has permissions to save this object.
  637. if ($user && !$this->isAuthorized('save', null, $user)) {
  638. throw new \RuntimeException('Forbidden', 403);
  639. }
  640. }
  641. /**
  642. * {@inheritdoc}
  643. * @see FlexObjectInterface::save()
  644. */
  645. public function save()
  646. {
  647. $this->triggerEvent('onBeforeSave');
  648. $storage = $this->getFlexDirectory()->getStorage();
  649. $storageKey = $this->getStorageKey() ?: '@@' . spl_object_hash($this);
  650. $result = $storage->replaceRows([$storageKey => $this->prepareStorage()]);
  651. if (method_exists($this, 'clearMediaCache')) {
  652. $this->clearMediaCache();
  653. }
  654. $value = reset($result);
  655. $meta = $value['__META'] ?? null;
  656. if ($meta) {
  657. /** @phpstan-var class-string $indexClass */
  658. $indexClass = $this->getFlexDirectory()->getIndexClass();
  659. $indexClass::updateObjectMeta($meta, $value, $storage);
  660. $this->_meta = $meta;
  661. }
  662. if ($value) {
  663. $storageKey = $meta['storage_key'] ?? (string)key($result);
  664. if ($storageKey !== '') {
  665. $this->setStorageKey($storageKey);
  666. }
  667. $newKey = $meta['key'] ?? ($this->hasKey() ? $this->getKey() : null);
  668. $this->setKey($newKey ?? $storageKey);
  669. }
  670. // FIXME: For some reason locator caching isn't cleared for the file, investigate!
  671. $locator = Grav::instance()['locator'];
  672. $locator->clearCache();
  673. if (method_exists($this, 'saveUpdatedMedia')) {
  674. $this->saveUpdatedMedia();
  675. }
  676. try {
  677. $this->getFlexDirectory()->reloadIndex();
  678. if (method_exists($this, 'clearMediaCache')) {
  679. $this->clearMediaCache();
  680. }
  681. } catch (Exception $e) {
  682. /** @var Debugger $debugger */
  683. $debugger = Grav::instance()['debugger'];
  684. $debugger->addException($e);
  685. // Caching failed, but we can ignore that for now.
  686. }
  687. $this->triggerEvent('onAfterSave');
  688. return $this;
  689. }
  690. /**
  691. * {@inheritdoc}
  692. * @see FlexObjectInterface::delete()
  693. */
  694. public function delete()
  695. {
  696. if (!$this->exists()) {
  697. return $this;
  698. }
  699. $this->triggerEvent('onBeforeDelete');
  700. $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
  701. try {
  702. $this->getFlexDirectory()->reloadIndex();
  703. if (method_exists($this, 'clearMediaCache')) {
  704. $this->clearMediaCache();
  705. }
  706. } catch (Exception $e) {
  707. /** @var Debugger $debugger */
  708. $debugger = Grav::instance()['debugger'];
  709. $debugger->addException($e);
  710. // Caching failed, but we can ignore that for now.
  711. }
  712. $this->triggerEvent('onAfterDelete');
  713. return $this;
  714. }
  715. /**
  716. * {@inheritdoc}
  717. * @see FlexObjectInterface::getBlueprint()
  718. */
  719. public function getBlueprint(string $name = '')
  720. {
  721. if (!isset($this->_blueprint[$name])) {
  722. $blueprint = $this->doGetBlueprint($name);
  723. $blueprint->setScope('object');
  724. $blueprint->setObject($this);
  725. $this->_blueprint[$name] = $blueprint->init();
  726. }
  727. return $this->_blueprint[$name];
  728. }
  729. /**
  730. * {@inheritdoc}
  731. * @see FlexObjectInterface::getForm()
  732. */
  733. public function getForm(string $name = '', array $options = null)
  734. {
  735. $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
  736. if (!isset($this->_forms[$hash])) {
  737. $this->_forms[$hash] = $this->createFormObject($name, $options);
  738. }
  739. return $this->_forms[$hash];
  740. }
  741. /**
  742. * {@inheritdoc}
  743. * @see FlexObjectInterface::getDefaultValue()
  744. */
  745. public function getDefaultValue(string $name, string $separator = null)
  746. {
  747. $separator = $separator ?: '.';
  748. $path = explode($separator, $name);
  749. $offset = array_shift($path);
  750. $current = $this->getDefaultValues();
  751. if (!isset($current[$offset])) {
  752. return null;
  753. }
  754. $current = $current[$offset];
  755. while ($path) {
  756. $offset = array_shift($path);
  757. if ((is_array($current) || $current instanceof ArrayAccess) && isset($current[$offset])) {
  758. $current = $current[$offset];
  759. } elseif (is_object($current) && isset($current->{$offset})) {
  760. $current = $current->{$offset};
  761. } else {
  762. return null;
  763. }
  764. };
  765. return $current;
  766. }
  767. /**
  768. * @return array
  769. */
  770. public function getDefaultValues(): array
  771. {
  772. return $this->getBlueprint()->getDefaults();
  773. }
  774. /**
  775. * {@inheritdoc}
  776. * @see FlexObjectInterface::getFormValue()
  777. */
  778. public function getFormValue(string $name, $default = null, string $separator = null)
  779. {
  780. if ($name === 'storage_key') {
  781. return $this->getStorageKey();
  782. }
  783. if ($name === 'storage_timestamp') {
  784. return $this->getTimestamp();
  785. }
  786. return $this->getNestedProperty($name, $default, $separator);
  787. }
  788. /**
  789. * @param FlexDirectory $directory
  790. */
  791. public function setFlexDirectory(FlexDirectory $directory): void
  792. {
  793. $this->_flexDirectory = $directory;
  794. }
  795. /**
  796. * Returns a string representation of this object.
  797. *
  798. * @return string
  799. */
  800. #[\ReturnTypeWillChange]
  801. public function __toString()
  802. {
  803. return $this->getFlexKey();
  804. }
  805. /**
  806. * @return array
  807. */
  808. #[\ReturnTypeWillChange]
  809. public function __debugInfo()
  810. {
  811. return [
  812. 'type:private' => $this->getFlexType(),
  813. 'storage_key:protected' => $this->getStorageKey(),
  814. 'storage_timestamp:protected' => $this->getTimestamp(),
  815. 'key:private' => $this->getKey(),
  816. 'elements:private' => $this->getElements(),
  817. 'storage:private' => $this->getMetaData()
  818. ];
  819. }
  820. /**
  821. * Clone object.
  822. */
  823. #[\ReturnTypeWillChange]
  824. public function __clone()
  825. {
  826. // Allows future compatibility as parent::__clone() works.
  827. }
  828. protected function markAsCopy(): void
  829. {
  830. $meta = $this->getMetaData();
  831. $meta['copy'] = true;
  832. $this->_meta = $meta;
  833. }
  834. /**
  835. * @param string $name
  836. * @return Blueprint
  837. */
  838. protected function doGetBlueprint(string $name = ''): Blueprint
  839. {
  840. return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name);
  841. }
  842. /**
  843. * @param array $meta
  844. */
  845. protected function setMetaData(array $meta): void
  846. {
  847. $this->_meta = $meta;
  848. }
  849. /**
  850. * @return array
  851. */
  852. protected function doSerialize(): array
  853. {
  854. return [
  855. 'type' => $this->getFlexType(),
  856. 'key' => $this->getKey(),
  857. 'elements' => $this->getElements(),
  858. 'storage' => $this->getMetaData()
  859. ];
  860. }
  861. /**
  862. * @param array $serialized
  863. * @param FlexDirectory|null $directory
  864. * @return void
  865. */
  866. protected function doUnserialize(array $serialized, FlexDirectory $directory = null): void
  867. {
  868. $type = $serialized['type'] ?? 'unknown';
  869. if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) {
  870. throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data");
  871. }
  872. if (null === $directory) {
  873. $directory = $this->getFlexContainer()->getDirectory($type);
  874. if (!$directory) {
  875. throw new \InvalidArgumentException("Cannot unserialize Flex type '{$type}': Directory not found");
  876. }
  877. }
  878. $this->setFlexDirectory($directory);
  879. $this->setMetaData($serialized['storage']);
  880. $this->setKey($serialized['key']);
  881. $this->setElements($serialized['elements']);
  882. }
  883. /**
  884. * @return array
  885. */
  886. protected function getTemplateConfig()
  887. {
  888. $config = $this->getFlexDirectory()->getConfig('site.templates', []);
  889. $defaults = array_replace($config['defaults'] ?? [], $config['object']['defaults'] ?? []);
  890. $config['object']['defaults'] = $defaults;
  891. return $config;
  892. }
  893. /**
  894. * @param string $layout
  895. * @return array
  896. */
  897. protected function getTemplatePaths(string $layout): array
  898. {
  899. $config = $this->getTemplateConfig();
  900. $type = $this->getFlexType();
  901. $defaults = $config['object']['defaults'] ?? [];
  902. $ext = $defaults['ext'] ?? '.html.twig';
  903. $types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null)));
  904. $paths = $config['object']['paths'] ?? [
  905. 'flex/{TYPE}/object/{LAYOUT}{EXT}',
  906. 'flex-objects/layouts/{TYPE}/object/{LAYOUT}{EXT}'
  907. ];
  908. $table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s'];
  909. $lookups = [];
  910. foreach ($paths as $path) {
  911. $path = Utils::simpleTemplate($path, $table);
  912. foreach ($types as $type) {
  913. $lookups[] = sprintf($path, $type, $layout, $ext);
  914. }
  915. }
  916. return array_unique($lookups);
  917. }
  918. /**
  919. * Filter data coming to constructor or $this->update() request.
  920. *
  921. * NOTE: The incoming data can be an arbitrary array so do not assume anything from its content.
  922. *
  923. * @param array $elements
  924. */
  925. protected function filterElements(array &$elements): void
  926. {
  927. if (isset($elements['storage_key'])) {
  928. $elements['storage_key'] = trim($elements['storage_key']);
  929. }
  930. if (isset($elements['storage_timestamp'])) {
  931. $elements['storage_timestamp'] = (int)$elements['storage_timestamp'];
  932. }
  933. unset($elements['_post_entries_save']);
  934. }
  935. /**
  936. * This methods allows you to override form objects in child classes.
  937. *
  938. * @param string $name Form name
  939. * @param array|null $options Form optiosn
  940. * @return FlexFormInterface
  941. */
  942. protected function createFormObject(string $name, array $options = null)
  943. {
  944. return new FlexForm($name, $this, $options);
  945. }
  946. /**
  947. * @param string $action
  948. * @return string
  949. */
  950. protected function getAuthorizeAction(string $action): string
  951. {
  952. // Handle special action save, which can mean either update or create.
  953. if ($action === 'save') {
  954. $action = $this->exists() ? 'update' : 'create';
  955. }
  956. return $action;
  957. }
  958. /**
  959. * Method to reset blueprints if the type changes.
  960. *
  961. * @return void
  962. * @since 1.7.18
  963. */
  964. protected function resetBlueprints(): void
  965. {
  966. $this->_blueprint = [];
  967. }
  968. // DEPRECATED METHODS
  969. /**
  970. * @param bool $prefix
  971. * @return string
  972. * @deprecated 1.6 Use `->getFlexType()` instead.
  973. */
  974. public function getType($prefix = false)
  975. {
  976. user_error(__METHOD__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
  977. $type = $prefix ? $this->getTypePrefix() : '';
  978. return $type . $this->getFlexType();
  979. }
  980. /**
  981. * @param string $name
  982. * @param mixed|null $default
  983. * @param string|null $separator
  984. * @return mixed
  985. *
  986. * @deprecated 1.6 Use ->getFormValue() method instead.
  987. */
  988. public function value($name, $default = null, $separator = null)
  989. {
  990. user_error(__METHOD__ . '() is deprecated since Grav 1.6, use ->getFormValue() method instead', E_USER_DEPRECATED);
  991. return $this->getFormValue($name, $default, $separator);
  992. }
  993. /**
  994. * @param string $name
  995. * @param object|null $event
  996. * @return $this
  997. * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\FlexObjectTrait
  998. */
  999. public function triggerEvent(string $name, $event = null)
  1000. {
  1001. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED);
  1002. if (null === $event) {
  1003. $event = new Event([
  1004. 'type' => 'flex',
  1005. 'directory' => $this->getFlexDirectory(),
  1006. 'object' => $this
  1007. ]);
  1008. }
  1009. if (strpos($name, 'onFlexObject') !== 0 && strpos($name, 'on') === 0) {
  1010. $name = 'onFlexObject' . substr($name, 2);
  1011. }
  1012. $grav = Grav::instance();
  1013. if ($event instanceof Event) {
  1014. $grav->fireEvent($name, $event);
  1015. } else {
  1016. $grav->dispatchEvent($event);
  1017. }
  1018. return $this;
  1019. }
  1020. /**
  1021. * @param array $storage
  1022. * @deprecated 1.7 Use `->setMetaData()` instead.
  1023. */
  1024. protected function setStorage(array $storage): void
  1025. {
  1026. user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->setMetaData() method instead', E_USER_DEPRECATED);
  1027. $this->setMetaData($storage);
  1028. }
  1029. /**
  1030. * @return array
  1031. * @deprecated 1.7 Use `->getMetaData()` instead.
  1032. */
  1033. protected function getStorage(): array
  1034. {
  1035. user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->getMetaData() method instead', E_USER_DEPRECATED);
  1036. return $this->getMetaData();
  1037. }
  1038. /**
  1039. * @param string $layout
  1040. * @return Template|TemplateWrapper
  1041. * @throws LoaderError
  1042. * @throws SyntaxError
  1043. * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait
  1044. */
  1045. protected function getTemplate($layout)
  1046. {
  1047. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED);
  1048. $grav = Grav::instance();
  1049. /** @var Twig $twig */
  1050. $twig = $grav['twig'];
  1051. try {
  1052. return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
  1053. } catch (LoaderError $e) {
  1054. /** @var Debugger $debugger */
  1055. $debugger = Grav::instance()['debugger'];
  1056. $debugger->addException($e);
  1057. return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
  1058. }
  1059. }
  1060. /**
  1061. * @return Flex
  1062. * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait
  1063. */
  1064. protected function getFlexContainer(): Flex
  1065. {
  1066. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED);
  1067. /** @var Flex $flex */
  1068. $flex = Grav::instance()['flex'];
  1069. return $flex;
  1070. }
  1071. /**
  1072. * @return UserInterface|null
  1073. * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait
  1074. */
  1075. protected function getActiveUser(): ?UserInterface
  1076. {
  1077. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED);
  1078. /** @var UserInterface|null $user */
  1079. $user = Grav::instance()['user'] ?? null;
  1080. return $user;
  1081. }
  1082. /**
  1083. * @return string
  1084. * @deprecated 1.7 Moved to \Grav\Common\Flex\Traits\GravTrait
  1085. */
  1086. protected function getAuthorizeScope(): string
  1087. {
  1088. user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\GravTrait', E_USER_DEPRECATED);
  1089. return isset(Grav::instance()['admin']) ? 'admin' : 'site';
  1090. }
  1091. }