123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877 |
- <?php
- /**
- * @package Grav\Framework\Flex
- *
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Framework\Flex;
- use Grav\Common\Data\Blueprint;
- use Grav\Common\Debugger;
- use Grav\Common\Grav;
- use Grav\Common\Twig\Twig;
- use Grav\Common\Utils;
- use Grav\Framework\Cache\CacheInterface;
- use Grav\Framework\ContentBlock\HtmlBlock;
- use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
- use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
- use Grav\Framework\Flex\Interfaces\FlexFormInterface;
- use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
- use Grav\Framework\Object\Access\NestedArrayAccessTrait;
- use Grav\Framework\Object\Access\NestedPropertyTrait;
- use Grav\Framework\Object\Access\OverloadedPropertyTrait;
- use Grav\Framework\Object\Base\ObjectTrait;
- use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
- use Grav\Framework\Object\Interfaces\ObjectInterface;
- use Grav\Framework\Object\Property\LazyPropertyTrait;
- use Psr\SimpleCache\InvalidArgumentException;
- use RocketTheme\Toolbox\Event\Event;
- use Twig\Error\LoaderError;
- use Twig\Error\SyntaxError;
- use Twig\Template;
- use Twig\TemplateWrapper;
- /**
- * Class FlexObject
- * @package Grav\Framework\Flex
- */
- class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
- {
- use ObjectTrait;
- use LazyPropertyTrait {
- LazyPropertyTrait::__construct as private objectConstruct;
- }
- use NestedPropertyTrait;
- use OverloadedPropertyTrait;
- use NestedArrayAccessTrait;
- use FlexAuthorizeTrait;
- /** @var FlexDirectory */
- private $_flexDirectory;
- /** @var FlexFormInterface[] */
- private $_forms = [];
- /** @var array */
- private $_storage;
- /** @var array */
- protected $_changes;
- /**
- * @return array
- */
- public static function getCachedMethods(): array
- {
- return [
- 'getTypePrefix' => true,
- 'getType' => true,
- 'getFlexType' => true,
- 'getFlexDirectory' => true,
- 'getCacheKey' => true,
- 'getCacheChecksum' => true,
- 'getTimestamp' => true,
- 'value' => true,
- 'exists' => true,
- 'hasProperty' => true,
- 'getProperty' => true,
- // FlexAclTrait
- 'isAuthorized' => 'session',
- ];
- }
- public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false)
- {
- $instance = new static($elements, $storage['key'], $directory, $validate);
- $instance->setStorage($storage);
- return $instance;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::__construct()
- */
- public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
- {
- $this->_flexDirectory = $directory;
- if ($validate) {
- $blueprint = $this->getFlexDirectory()->getBlueprint();
- $blueprint->validate($elements);
- $elements = $blueprint->filter($elements);
- }
- $this->filterElements($elements);
- $this->objectConstruct($elements, $key);
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getFlexType()
- */
- public function getFlexType(): string
- {
- return $this->_flexDirectory->getFlexType();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getFlexDirectory()
- */
- public function getFlexDirectory(): FlexDirectory
- {
- return $this->_flexDirectory;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getTimestamp()
- */
- public function getTimestamp(): int
- {
- return $this->_storage['storage_timestamp'] ?? 0;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getCacheKey()
- */
- public function getCacheKey(): string
- {
- return $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getStorageKey();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getCacheChecksum()
- */
- public function getCacheChecksum(): string
- {
- return (string)$this->getTimestamp();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::search()
- */
- public function search(string $search, $properties = null, array $options = null): float
- {
- $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
- $properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []);
- if (!$properties) {
- foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) {
- if (!empty($value['link'])) {
- $properties[] = $property;
- }
- }
- }
- $weight = 0;
- foreach ((array)$properties as $property) {
- $weight += $this->searchNestedProperty($property, $search, $options);
- }
- return $weight > 0 ? min($weight, 1) : 0;
- }
- /**
- * {@inheritdoc}
- * @see ObjectInterface::getFlexKey()
- */
- public function getKey()
- {
- return $this->_key ?: $this->getFlexType() . '@@' . spl_object_hash($this);
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getFlexKey()
- */
- public function getFlexKey(): string
- {
- return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getStorageKey()
- */
- public function getStorageKey(): string
- {
- return $this->_storage['storage_key'] ?? $this->getTypePrefix() . $this->getFlexType() . '@@' . spl_object_hash($this);
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getMetaData()
- */
- public function getMetaData(): array
- {
- return $this->getStorage();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::exists()
- */
- public function exists(): bool
- {
- $key = $this->getStorageKey();
- return $key && $this->getFlexDirectory()->getStorage()->hasKey($key);
- }
- /**
- * @param string $property
- * @param string $search
- * @param array|null $options
- * @return float
- */
- public function searchProperty(string $property, string $search, array $options = null): float
- {
- $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
- $value = $this->getProperty($property);
- return $this->searchValue($property, $value, $search, $options);
- }
- /**
- * @param string $property
- * @param string $search
- * @param array|null $options
- * @return float
- */
- public function searchNestedProperty(string $property, string $search, array $options = null): float
- {
- $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
- $value = $this->getNestedProperty($property);
- return $this->searchValue($property, $value, $search, $options);
- }
- /**
- * @param string $name
- * @param mixed $value
- * @param string $search
- * @param array|null $options
- * @return float
- */
- protected function searchValue(string $name, $value, string $search, array $options = null): float
- {
- $search = trim($search);
- if ($search === '') {
- return 0;
- }
- if (!\is_string($value) || $value === '') {
- return 0;
- }
- $tested = false;
- if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $options['case_sensitive'] ?? false)) {
- return (float)$options['starts_with'];
- }
- if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $options['case_sensitive'] ?? false)) {
- return (float)$options['ends_with'];
- }
- if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) {
- return (float)($options['contains'] ?? 1);
- }
- return 0;
- }
- /**
- * Get any changes based on data sent to update
- *
- * @return array
- */
- public function getChanges(): array
- {
- return $this->_changes ?? [];
- }
- /**
- * @return string
- */
- protected function getTypePrefix(): string
- {
- return 'o.';
- }
- /**
- * @param bool $prefix
- * @return string
- * @deprecated 1.6 Use `->getFlexType()` instead.
- */
- public function getType($prefix = false)
- {
- user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
- $type = $prefix ? $this->getTypePrefix() : '';
- return $type . $this->getFlexType();
- }
- /**
- * Alias of getBlueprint()
- *
- * @return Blueprint
- * @deprecated 1.6 Admin compatibility
- */
- public function blueprints()
- {
- return $this->getBlueprint();
- }
- /**
- * @param string|null $namespace
- * @return CacheInterface
- */
- public function getCache(string $namespace = null)
- {
- return $this->_flexDirectory->getCache($namespace);
- }
- /**
- * @param string|null $key
- * @return $this
- */
- public function setStorageKey($key = null)
- {
- $this->_storage['storage_key'] = $key;
- return $this;
- }
- /**
- * @param int $timestamp
- * @return $this
- */
- public function setTimestamp($timestamp = null)
- {
- $this->_storage['storage_timestamp'] = $timestamp ?? time();
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::render()
- */
- public function render(string $layout = null, array $context = [])
- {
- if (null === $layout) {
- $layout = 'default';
- }
- $type = $this->getFlexType();
- $grav = Grav::instance();
- /** @var Debugger $debugger */
- $debugger = $grav['debugger'];
- $debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')');
- $cache = $key = null;
- foreach ($context as $value) {
- if (!\is_scalar($value)) {
- $key = false;
- }
- }
- if ($key !== false) {
- $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
- $cache = $this->getCache('render');
- }
- try {
- $data = $cache && $key ? $cache->get($key) : null;
- $block = $data ? HtmlBlock::fromArray($data) : null;
- } catch (InvalidArgumentException $e) {
- $debugger->addException($e);
- $block = null;
- } catch (\InvalidArgumentException $e) {
- $debugger->addException($e);
- $block = null;
- }
- $checksum = $this->getCacheChecksum();
- if ($block && $checksum !== $block->getChecksum()) {
- $block = null;
- }
- if (!$block) {
- $block = HtmlBlock::create($key ?: null);
- $block->setChecksum($checksum);
- if ($key === false) {
- $block->disableCache();
- }
- $grav->fireEvent('onFlexObjectRender', new Event([
- 'object' => $this,
- 'layout' => &$layout,
- 'context' => &$context
- ]));
- $output = $this->getTemplate($layout)->render(
- ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'object' => $this, 'layout' => $layout] + $context
- );
- if ($debugger->enabled()) {
- $name = $this->getKey() . ' (' . $type . ')';
- $output = "\n<!–– START {$name} object ––>\n{$output}\n<!–– END {$name} object ––>\n";
- }
- $block->setContent($output);
- try {
- $cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
- } catch (InvalidArgumentException $e) {
- $debugger->addException($e);
- }
- }
- $debugger->stopTimer('flex-object-' . $debugKey);
- return $block;
- }
- /**
- * @return array
- */
- public function jsonSerialize()
- {
- return $this->getElements();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::prepareStorage()
- */
- public function prepareStorage(): array
- {
- return $this->getElements();
- }
- /**
- * @param string $name
- * @return $this
- */
- public function triggerEvent($name)
- {
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::update()
- */
- public function update(array $data, array $files = [])
- {
- if ($data) {
- $blueprint = $this->getBlueprint();
- // Process updated data through the object filters.
- $this->filterElements($data);
- // Get currently stored data.
- $elements = $this->getElements();
- // Merge existing object to the test data to be validated.
- $test = $blueprint->mergeData($elements, $data);
- // Validate and filter elements and throw an error if any issues were found.
- $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]);
- $data = $blueprint->filter($data, false, true);
- // Finally update the object.
- foreach ($blueprint->flattenData($data) as $key => $value) {
- if ($value === null) {
- $this->unsetNestedProperty($key);
- } else {
- $this->setNestedProperty($key, $value);
- }
- }
- // Store the changes
- $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
- }
- if ($files && method_exists($this, 'setUpdatedMedia')) {
- $this->setUpdatedMedia($files);
- }
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::create()
- */
- public function create(string $key = null)
- {
- if ($key) {
- $this->setStorageKey($key);
- }
- if ($this->exists()) {
- throw new \RuntimeException('Cannot create new object (Already exists)');
- }
- return $this->save();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::save()
- */
- public function save()
- {
- $this->triggerEvent('onBeforeSave');
- $result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
- $value = reset($result);
- $storageKey = (string)key($result);
- if ($value && $storageKey) {
- $this->setStorageKey($storageKey);
- if (!$this->hasKey()) {
- $this->setKey($storageKey);
- }
- }
- // FIXME: For some reason locator caching isn't cleared for the file, investigate!
- $locator = Grav::instance()['locator'];
- $locator->clearCache();
- // Make sure that the object exists before continuing (just in case).
- if (!$this->exists()) {
- throw new \RuntimeException('Saving failed: Object does not exist!');
- }
- if (method_exists($this, 'saveUpdatedMedia')) {
- $this->saveUpdatedMedia();
- }
- try {
- $this->getFlexDirectory()->clearCache();
- if (method_exists($this, 'clearMediaCache')) {
- $this->clearMediaCache();
- }
- } catch (\Exception $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- // Caching failed, but we can ignore that for now.
- }
- $this->triggerEvent('onAfterSave');
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::delete()
- */
- public function delete()
- {
- $this->triggerEvent('onBeforeDelete');
- $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
- try {
- $this->getFlexDirectory()->clearCache();
- if (method_exists($this, 'clearMediaCache')) {
- $this->clearMediaCache();
- }
- } catch (\Exception $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- // Caching failed, but we can ignore that for now.
- }
- $this->triggerEvent('onAfterDelete');
- return $this;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getBlueprint()
- */
- public function getBlueprint(string $name = '')
- {
- return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name);
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getForm()
- */
- public function getForm(string $name = '', array $form = null)
- {
- if (!isset($this->_forms[$name])) {
- $this->_forms[$name] = $this->createFormObject($name, $form);
- }
- return $this->_forms[$name];
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getDefaultValue()
- */
- public function getDefaultValue(string $name, string $separator = null)
- {
- $separator = $separator ?: '.';
- $path = explode($separator, $name) ?: [];
- $offset = array_shift($path) ?? '';
- $current = $this->getDefaultValues();
- if (!isset($current[$offset])) {
- return null;
- }
- $current = $current[$offset];
- while ($path) {
- $offset = array_shift($path);
- if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
- $current = $current[$offset];
- } elseif (\is_object($current) && isset($current->{$offset})) {
- $current = $current->{$offset};
- } else {
- return null;
- }
- };
- return $current;
- }
- /**
- * @return array
- */
- public function getDefaultValues(): array
- {
- return $this->getBlueprint()->getDefaults();
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getFormValue()
- */
- public function getFormValue(string $name, $default = null, string $separator = null)
- {
- if ($name === 'storage_key') {
- return $this->getStorageKey();
- }
- if ($name === 'storage_timestamp') {
- return $this->getTimestamp();
- }
- return $this->getNestedProperty($name, $default, $separator);
- }
- /**
- * @param string $name
- * @param mixed|null $default
- * @param string|null $separator
- * @return mixed
- *
- * @deprecated 1.6 Use ->getFormValue() method instead.
- */
- public function value($name, $default = null, $separator = null)
- {
- return $this->getFormValue($name, $default, $separator);
- }
- /**
- * Returns a string representation of this object.
- *
- * @return string
- */
- public function __toString()
- {
- return $this->getFlexKey();
- }
- public function __debugInfo()
- {
- return [
- 'type:private' => $this->getFlexType(),
- 'key:private' => $this->getKey(),
- 'elements:private' => $this->getElements(),
- 'storage:private' => $this->getStorage()
- ];
- }
- /**
- * @return array
- */
- protected function doSerialize(): array
- {
- return [
- 'type' => $this->getFlexType(),
- 'key' => $this->getKey(),
- 'elements' => $this->getElements(),
- 'storage' => $this->getStorage()
- ];
- }
- /**
- * @param array $serialized
- */
- protected function doUnserialize(array $serialized): void
- {
- $type = $serialized['type'] ?? 'unknown';
- if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) {
- throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data");
- }
- $grav = Grav::instance();
- /** @var Flex|null $flex */
- $flex = $grav['flex_objects'] ?? null;
- $directory = $flex ? $flex->getDirectory($type) : null;
- if (!$directory) {
- throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found");
- }
- $this->setFlexDirectory($directory);
- $this->setStorage($serialized['storage']);
- $this->setKey($serialized['key']);
- $this->setElements($serialized['elements']);
- }
- /**
- * @param FlexDirectory $directory
- */
- public function setFlexDirectory(FlexDirectory $directory): void
- {
- $this->_flexDirectory = $directory;
- }
- /**
- * @param array $storage
- */
- protected function setStorage(array $storage) : void
- {
- $this->_storage = $storage;
- }
- /**
- * @return array
- */
- protected function getStorage() : array
- {
- return $this->_storage ?? [];
- }
- /**
- * @param string $type
- * @param string $property
- * @return FlexCollectionInterface
- */
- protected function getCollectionByProperty($type, $property)
- {
- $directory = $this->getRelatedDirectory($type);
- $collection = $directory->getCollection();
- $list = $this->getNestedProperty($property) ?: [];
- /** @var FlexCollection $collection */
- $collection = $collection->filter(function ($object) use ($list) { return \in_array($object->id, $list, true); });
- return $collection;
- }
- /**
- * @param string $type
- * @return FlexDirectory
- * @throws \RuntimeException
- */
- protected function getRelatedDirectory($type): FlexDirectory
- {
- /** @var Flex $flex */
- $flex = Grav::instance()['flex_objects'];
- $directory = $flex->getDirectory($type);
- if (!$directory) {
- throw new \RuntimeException(ucfirst($type). ' directory does not exist!');
- }
- return $directory;
- }
- /**
- * @param string $layout
- * @return Template|TemplateWrapper
- * @throws LoaderError
- * @throws SyntaxError
- */
- protected function getTemplate($layout)
- {
- $grav = Grav::instance();
- /** @var Twig $twig */
- $twig = $grav['twig'];
- try {
- return $twig->twig()->resolveTemplate(
- [
- "flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig",
- "flex-objects/layouts/_default/object/{$layout}.html.twig"
- ]
- );
- } catch (LoaderError $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']);
- }
- }
- /**
- * Filter data coming to constructor or $this->update() request.
- *
- * NOTE: The incoming data can be an arbitrary array so do not assume anything from its content.
- *
- * @param array $elements
- */
- protected function filterElements(array &$elements): void
- {
- if (!empty($elements['storage_key'])) {
- $this->_storage['storage_key'] = trim($elements['storage_key']);
- }
- if (!empty($elements['storage_timestamp'])) {
- $this->_storage['storage_timestamp'] = (int)$elements['storage_timestamp'];
- }
- unset ($elements['storage_key'], $elements['storage_timestamp'], $elements['_post_entries_save']);
- }
- /**
- * This methods allows you to override form objects in child classes.
- *
- * @param string $name Form name
- * @param array|null $form Form fields
- * @return FlexFormInterface
- */
- protected function createFormObject(string $name, array $form = null)
- {
- return new FlexForm($name, $this, $form);
- }
- }
|