PageObject.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Common\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Common\Flex\Types\Pages;
  10. use Grav\Common\Data\Blueprint;
  11. use Grav\Common\Flex\Traits\FlexGravTrait;
  12. use Grav\Common\Flex\Traits\FlexObjectTrait;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Flex\Types\Pages\Traits\PageContentTrait;
  15. use Grav\Common\Flex\Types\Pages\Traits\PageLegacyTrait;
  16. use Grav\Common\Flex\Types\Pages\Traits\PageRoutableTrait;
  17. use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait;
  18. use Grav\Common\Language\Language;
  19. use Grav\Common\Page\Interfaces\PageInterface;
  20. use Grav\Common\Page\Pages;
  21. use Grav\Common\User\Interfaces\UserInterface;
  22. use Grav\Common\Utils;
  23. use Grav\Framework\Filesystem\Filesystem;
  24. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  25. use Grav\Framework\Flex\Pages\FlexPageObject;
  26. use Grav\Framework\Object\ObjectCollection;
  27. use Grav\Framework\Route\Route;
  28. use Grav\Framework\Route\RouteFactory;
  29. use Grav\Plugin\Admin\Admin;
  30. use RocketTheme\Toolbox\Event\Event;
  31. use RuntimeException;
  32. use stdClass;
  33. use function array_key_exists;
  34. use function count;
  35. use function func_get_args;
  36. use function in_array;
  37. use function is_array;
  38. /**
  39. * Class GravPageObject
  40. * @package Grav\Plugin\FlexObjects\Types\GravPages
  41. *
  42. * @property string $name
  43. * @property string $slug
  44. * @property string $route
  45. * @property string $folder
  46. * @property int|false $order
  47. * @property string $template
  48. * @property string $language
  49. */
  50. class PageObject extends FlexPageObject
  51. {
  52. use FlexGravTrait;
  53. use FlexObjectTrait;
  54. use PageContentTrait;
  55. use PageLegacyTrait;
  56. use PageRoutableTrait;
  57. use PageTranslateTrait;
  58. /** @var string Language code, eg: 'en' */
  59. protected $language;
  60. /** @var string File format, eg. 'md' */
  61. protected $format;
  62. /** @var bool */
  63. private $_initialized = false;
  64. /**
  65. * @return array
  66. */
  67. public static function getCachedMethods(): array
  68. {
  69. return [
  70. 'path' => true,
  71. 'full_order' => true,
  72. 'filterBy' => true,
  73. 'translated' => false,
  74. ] + parent::getCachedMethods();
  75. }
  76. /**
  77. * @return void
  78. */
  79. public function initialize(): void
  80. {
  81. if (!$this->_initialized) {
  82. Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
  83. $this->_initialized = true;
  84. }
  85. }
  86. /**
  87. * @param string|array $query
  88. * @return Route|null
  89. */
  90. public function getRoute($query = []): ?Route
  91. {
  92. $path = $this->route();
  93. if (null === $path) {
  94. return null;
  95. }
  96. $route = RouteFactory::createFromString($path);
  97. if ($lang = $route->getLanguage()) {
  98. $grav = Grav::instance();
  99. if (!$grav['config']->get('system.languages.include_default_lang')) {
  100. /** @var Language $language */
  101. $language = $grav['language'];
  102. if ($lang === $language->getDefault()) {
  103. $route = $route->withLanguage('');
  104. }
  105. }
  106. }
  107. if (is_array($query)) {
  108. foreach ($query as $key => $value) {
  109. $route = $route->withQueryParam($key, $value);
  110. }
  111. } else {
  112. $route = $route->withAddedPath($query);
  113. }
  114. return $route;
  115. }
  116. /**
  117. * @inheritdoc PageInterface
  118. */
  119. public function getFormValue(string $name, $default = null, string $separator = null)
  120. {
  121. $test = new stdClass();
  122. $value = $this->pageContentValue($name, $test);
  123. if ($value !== $test) {
  124. return $value;
  125. }
  126. switch ($name) {
  127. case 'name':
  128. // TODO: this should not be template!
  129. return $this->getProperty('template');
  130. case 'route':
  131. $filesystem = Filesystem::getInstance(false);
  132. $key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/');
  133. return $key !== '/' ? $key : null;
  134. case 'full_route':
  135. return $this->hasKey() ? '/' . $this->getKey() : '';
  136. case 'full_order':
  137. return $this->full_order();
  138. case 'lang':
  139. return $this->getLanguage() ?? '';
  140. case 'translations':
  141. return $this->getLanguages();
  142. }
  143. return parent::getFormValue($name, $default, $separator);
  144. }
  145. /**
  146. * {@inheritdoc}
  147. * @see FlexObjectInterface::getCacheKey()
  148. */
  149. public function getCacheKey(): string
  150. {
  151. $cacheKey = parent::getCacheKey();
  152. if ($cacheKey) {
  153. /** @var Language $language */
  154. $language = Grav::instance()['language'];
  155. $cacheKey .= '_' . $language->getActive();
  156. }
  157. return $cacheKey;
  158. }
  159. /**
  160. * @param array $variables
  161. * @return array
  162. */
  163. protected function onBeforeSave(array $variables)
  164. {
  165. $reorder = $variables[0] ?? true;
  166. $meta = $this->getMetaData();
  167. if (($meta['copy'] ?? false) === true) {
  168. $this->folder = $this->getKey();
  169. }
  170. // Figure out storage path to the new route.
  171. $parentKey = $this->getProperty('parent_key');
  172. if ($parentKey !== '') {
  173. $parentRoute = $this->getProperty('route');
  174. // Root page cannot be moved.
  175. if ($this->root()) {
  176. throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute));
  177. }
  178. // Make sure page isn't being moved under itself.
  179. $key = $this->getStorageKey();
  180. /** @var PageObject|null $parent */
  181. $parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null;
  182. if (!$parent) {
  183. // Page cannot be moved to non-existing location.
  184. throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute));
  185. }
  186. // TODO: make sure that the page doesn't exist yet if moved/copied.
  187. }
  188. if ($reorder === true && !$this->root()) {
  189. $reorder = $this->_reorder;
  190. }
  191. // Force automatic reorder if item is supposed to be added to the last.
  192. if (!is_array($reorder) && (int)$this->order() >= 999999) {
  193. $reorder = [];
  194. }
  195. // Reorder siblings.
  196. $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
  197. $data = $this->prepareStorage();
  198. unset($data['header']);
  199. foreach ($siblings as $sibling) {
  200. $data = $sibling->prepareStorage();
  201. unset($data['header']);
  202. }
  203. return ['reorder' => $reorder, 'siblings' => $siblings];
  204. }
  205. /**
  206. * @param array $variables
  207. * @return array
  208. */
  209. protected function onSave(array $variables): array
  210. {
  211. /** @var PageCollection $siblings */
  212. $siblings = $variables['siblings'];
  213. /** @var PageObject $sibling */
  214. foreach ($siblings as $sibling) {
  215. $sibling->save(false);
  216. }
  217. return $variables;
  218. }
  219. /**
  220. * @param array $variables
  221. */
  222. protected function onAfterSave(array $variables): void
  223. {
  224. $this->getFlexDirectory()->reloadIndex();
  225. }
  226. /**
  227. * @param UserInterface|null $user
  228. */
  229. public function check(UserInterface $user = null): void
  230. {
  231. parent::check($user);
  232. if ($user && $this->isMoved()) {
  233. $parentKey = $this->getProperty('parent_key');
  234. /** @var PageObject|null $parent */
  235. $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
  236. if (!$parent || !$parent->isAuthorized('create', null, $user)) {
  237. throw new \RuntimeException('Forbidden', 403);
  238. }
  239. }
  240. }
  241. /**
  242. * @param array|bool $reorder
  243. * @return static
  244. */
  245. public function save($reorder = true)
  246. {
  247. $variables = $this->onBeforeSave(func_get_args());
  248. // Backwards compatibility with older plugins.
  249. $fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
  250. $grav = $this->getContainer();
  251. if ($fireEvents) {
  252. $self = $this;
  253. $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
  254. if ($self !== $this) {
  255. throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.');
  256. }
  257. }
  258. /** @var static $instance */
  259. $instance = parent::save();
  260. $variables = $this->onSave($variables);
  261. $this->onAfterSave($variables);
  262. // Backwards compatibility with older plugins.
  263. if ($fireEvents) {
  264. $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
  265. }
  266. // Reset original after save events have all been called.
  267. $this->_originalObject = null;
  268. return $instance;
  269. }
  270. /**
  271. * @return static
  272. */
  273. public function delete()
  274. {
  275. $result = parent::delete();
  276. // Backwards compatibility with older plugins.
  277. $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
  278. if ($fireEvents) {
  279. $this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this]));
  280. }
  281. return $result;
  282. }
  283. /**
  284. * Prepare move page to new location. Moves also everything that's under the current page.
  285. *
  286. * You need to call $this->save() in order to perform the move.
  287. *
  288. * @param PageInterface $parent New parent page.
  289. * @return $this
  290. */
  291. public function move(PageInterface $parent)
  292. {
  293. if (!$parent instanceof FlexObjectInterface) {
  294. throw new RuntimeException('Failed: Parent is not Flex Object');
  295. }
  296. $this->_reorder = [];
  297. $this->setProperty('parent_key', $parent->getStorageKey());
  298. $this->storeOriginal();
  299. return $this;
  300. }
  301. /**
  302. * @param UserInterface $user
  303. * @param string $action
  304. * @param string $scope
  305. * @param bool $isMe
  306. * @return bool|null
  307. */
  308. protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
  309. {
  310. // Special case: creating a new page means checking parent for its permissions.
  311. if ($action === 'create' && !$this->exists()) {
  312. $parent = $this->parent();
  313. if ($parent && method_exists($parent, 'isAuthorized')) {
  314. return $parent->isAuthorized($action, $scope, $user);
  315. }
  316. return false;
  317. }
  318. return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
  319. }
  320. /**
  321. * @return bool
  322. */
  323. protected function isMoved(): bool
  324. {
  325. $storageKey = $this->getMasterKey();
  326. $filesystem = Filesystem::getInstance(false);
  327. $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
  328. $newParentKey = $this->getProperty('parent_key');
  329. return $this->exists() && $oldParentKey !== $newParentKey;
  330. }
  331. /**
  332. * @param array $ordering
  333. * @return PageCollection|null
  334. * @phpstan-return ObjectCollection<string,PageObject>|null
  335. */
  336. protected function reorderSiblings(array $ordering)
  337. {
  338. $storageKey = $this->getMasterKey();
  339. $isMoved = $this->isMoved();
  340. $order = !$isMoved ? $this->order() : false;
  341. if ($order !== false) {
  342. $order = (int)$order;
  343. }
  344. $parent = $this->parent();
  345. if (!$parent) {
  346. throw new RuntimeException('Cannot reorder a page which has no parent');
  347. }
  348. /** @var PageCollection $siblings */
  349. $siblings = $parent->children();
  350. $siblings = $siblings->getCollection()->withOrdered();
  351. // Handle special case where ordering isn't given.
  352. if ($ordering === []) {
  353. if ($order >= 999999) {
  354. // Set ordering to point to be the last item, ignoring the object itself.
  355. $order = 0;
  356. foreach ($siblings as $sibling) {
  357. if ($sibling->getKey() !== $this->getKey()) {
  358. $order = max($order, (int)$sibling->order());
  359. }
  360. }
  361. $this->order($order + 1);
  362. }
  363. // Do not change sibling ordering.
  364. return null;
  365. }
  366. $siblings = $siblings->orderBy(['order' => 'ASC']);
  367. if ($storageKey !== null) {
  368. if ($order !== false) {
  369. // Add current page back to the list if it's ordered.
  370. $siblings->set($storageKey, $this);
  371. } else {
  372. // Remove old copy of the current page from the siblings list.
  373. $siblings->remove($storageKey);
  374. }
  375. }
  376. // Add missing siblings into the end of the list, keeping the previous ordering between them.
  377. foreach ($siblings as $sibling) {
  378. $folder = (string)$sibling->getProperty('folder');
  379. $basename = preg_replace('|^\d+\.|', '', $folder);
  380. if (!in_array($basename, $ordering, true)) {
  381. $ordering[] = $basename;
  382. }
  383. }
  384. // Reorder.
  385. $ordering = array_flip(array_values($ordering));
  386. $count = count($ordering);
  387. foreach ($siblings as $sibling) {
  388. $folder = (string)$sibling->getProperty('folder');
  389. $basename = preg_replace('|^\d+\.|', '', $folder);
  390. $newOrder = $ordering[$basename] ?? null;
  391. $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
  392. $sibling->order($newOrder);
  393. }
  394. $siblings = $siblings->orderBy(['order' => 'ASC']);
  395. $siblings->removeElement($this);
  396. // If menu item was moved, just make it to be the last in order.
  397. if ($isMoved && $this->order() !== false) {
  398. $parentKey = $this->getProperty('parent_key');
  399. if ($parentKey === '') {
  400. /** @var PageIndex $index */
  401. $index = $this->getFlexDirectory()->getIndex();
  402. $newParent = $index->getRoot();
  403. } else {
  404. $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
  405. if (!$newParent instanceof PageInterface) {
  406. throw new RuntimeException("New parent page '{$parentKey}' not found.");
  407. }
  408. }
  409. /** @var PageCollection $newSiblings */
  410. $newSiblings = $newParent->children();
  411. $newSiblings = $newSiblings->getCollection()->withOrdered();
  412. $order = 0;
  413. foreach ($newSiblings as $sibling) {
  414. $order = max($order, (int)$sibling->order());
  415. }
  416. $this->order($order + 1);
  417. }
  418. return $siblings;
  419. }
  420. /**
  421. * @return string
  422. */
  423. public function full_order(): string
  424. {
  425. $route = $this->path() . '/' . $this->folder();
  426. return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route;
  427. }
  428. /**
  429. * @param string $name
  430. * @return Blueprint
  431. */
  432. protected function doGetBlueprint(string $name = ''): Blueprint
  433. {
  434. try {
  435. // Make sure that pages has been initialized.
  436. Pages::getTypes();
  437. // TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here.
  438. if ($name === 'raw') {
  439. // Admin RAW mode.
  440. if ($this->isAdminSite()) {
  441. /** @var Admin $admin */
  442. $admin = Grav::instance()['admin'];
  443. $template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw');
  444. return $admin->blueprints("admin/pages/{$template}");
  445. }
  446. }
  447. $template = $this->getProperty('template') . ($name ? '.' . $name : '');
  448. $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
  449. } catch (RuntimeException $e) {
  450. $template = 'default' . ($name ? '.' . $name : '');
  451. $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
  452. }
  453. $isNew = $blueprint->get('initialized', false) === false;
  454. if ($isNew === true && $name === '') {
  455. // Support onBlueprintCreated event just like in Pages::blueprints($template)
  456. $blueprint->set('initialized', true);
  457. $blueprint->setFilename($template);
  458. Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
  459. }
  460. return $blueprint;
  461. }
  462. /**
  463. * @param array $options
  464. * @return array
  465. */
  466. public function getLevelListing(array $options): array
  467. {
  468. $index = $this->getFlexDirectory()->getIndex();
  469. if (!is_callable([$index, 'getLevelListing'])) {
  470. return [];
  471. }
  472. // Deal with relative paths.
  473. $initial = $options['initial'] ?? null;
  474. $var = $initial ? 'leaf_route' : 'route';
  475. $route = $options[$var] ?? '';
  476. if ($route !== '' && !str_starts_with($route, '/')) {
  477. $filesystem = Filesystem::getInstance();
  478. $route = "/{$this->getKey()}/{$route}";
  479. $route = $filesystem->normalize($route);
  480. $options[$var] = $route;
  481. }
  482. [$status, $message, $response,] = $index->getLevelListing($options);
  483. return [$status, $message, $response, $options[$var] ?? null];
  484. }
  485. /**
  486. * Filter page (true/false) by given filters.
  487. *
  488. * - search: string
  489. * - extension: string
  490. * - module: bool
  491. * - visible: bool
  492. * - routable: bool
  493. * - published: bool
  494. * - page: bool
  495. * - translated: bool
  496. *
  497. * @param array $filters
  498. * @param bool $recursive
  499. * @return bool
  500. */
  501. public function filterBy(array $filters, bool $recursive = false): bool
  502. {
  503. $language = $filters['language'] ?? null;
  504. if (null !== $language) {
  505. /** @var PageObject $test */
  506. $test = $this->getTranslation($language) ?? $this;
  507. } else {
  508. $test = $this;
  509. }
  510. foreach ($filters as $key => $value) {
  511. switch ($key) {
  512. case 'search':
  513. $matches = $test->search((string)$value) > 0.0;
  514. break;
  515. case 'page_type':
  516. $types = $value ? explode(',', $value) : [];
  517. $matches = in_array($test->template(), $types, true);
  518. break;
  519. case 'extension':
  520. $matches = Utils::contains((string)$value, $test->extension());
  521. break;
  522. case 'routable':
  523. $matches = $test->isRoutable() === (bool)$value;
  524. break;
  525. case 'published':
  526. $matches = $test->isPublished() === (bool)$value;
  527. break;
  528. case 'visible':
  529. $matches = $test->isVisible() === (bool)$value;
  530. break;
  531. case 'module':
  532. $matches = $test->isModule() === (bool)$value;
  533. break;
  534. case 'page':
  535. $matches = $test->isPage() === (bool)$value;
  536. break;
  537. case 'folder':
  538. $matches = $test->isPage() === !$value;
  539. break;
  540. case 'translated':
  541. $matches = $test->hasTranslation() === (bool)$value;
  542. break;
  543. default:
  544. $matches = true;
  545. break;
  546. }
  547. // If current filter does not match, we still may have match as a parent.
  548. if ($matches === false) {
  549. if (!$recursive) {
  550. return false;
  551. }
  552. /** @var PageIndex $index */
  553. $index = $this->children()->getIndex();
  554. return $index->filterBy($filters, true)->count() > 0;
  555. }
  556. }
  557. return true;
  558. }
  559. /**
  560. * {@inheritdoc}
  561. * @see FlexObjectInterface::exists()
  562. */
  563. public function exists(): bool
  564. {
  565. return $this->root ?: parent::exists();
  566. }
  567. /**
  568. * @return array
  569. */
  570. public function __debugInfo(): array
  571. {
  572. $list = parent::__debugInfo();
  573. return $list + [
  574. '_content_meta:private' => $this->getContentMeta(),
  575. '_content:private' => $this->getRawContent()
  576. ];
  577. }
  578. /**
  579. * @param array $elements
  580. * @param bool $extended
  581. */
  582. protected function filterElements(array &$elements, bool $extended = false): void
  583. {
  584. // Change parent page if needed.
  585. if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) {
  586. $elements['template'] = $elements['name'];
  587. // Figure out storage path to the new route.
  588. $parentKey = trim($elements['route'] ?? '', '/');
  589. if ($parentKey !== '') {
  590. /** @var PageObject|null $parent */
  591. $parent = $this->getFlexDirectory()->getObject($parentKey);
  592. $parentKey = $parent ? $parent->getStorageKey() : $parentKey;
  593. }
  594. $elements['parent_key'] = $parentKey;
  595. }
  596. // Deal with ordering=bool and order=page1,page2,page3.
  597. if ($this->root()) {
  598. // Root page doesn't have ordering.
  599. unset($elements['ordering'], $elements['order']);
  600. } elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
  601. // Store ordering.
  602. $ordering = $elements['order'] ?? null;
  603. $this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
  604. $order = false;
  605. if ((bool)($elements['ordering'] ?? false)) {
  606. $order = $this->order();
  607. if ($order === false) {
  608. $order = 999999;
  609. }
  610. }
  611. $elements['order'] = $order;
  612. }
  613. parent::filterElements($elements, true);
  614. }
  615. /**
  616. * @return array
  617. */
  618. public function prepareStorage(): array
  619. {
  620. $meta = $this->getMetaData();
  621. $oldLang = $meta['lang'] ?? '';
  622. $newLang = $this->getProperty('lang') ?? '';
  623. // Always clone the page to the new language.
  624. if ($oldLang !== $newLang) {
  625. $meta['clone'] = true;
  626. }
  627. // Make sure that certain elements are always sent to the storage layer.
  628. $elements = [
  629. '__META' => $meta,
  630. 'storage_key' => $this->getStorageKey(),
  631. 'parent_key' => $this->getProperty('parent_key'),
  632. 'order' => $this->getProperty('order'),
  633. 'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
  634. 'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
  635. 'lang' => $newLang
  636. ] + parent::prepareStorage();
  637. return $elements;
  638. }
  639. }