FlexObject.php 35 KB

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