FlexObject.php 33 KB

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