1078 lines
32 KiB
PHP
1078 lines
32 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package Grav\Framework\Flex
|
|
*
|
|
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
|
* @license MIT License; see LICENSE file for details.
|
|
*/
|
|
|
|
namespace Grav\Framework\Flex;
|
|
|
|
use Exception;
|
|
use Grav\Common\Cache;
|
|
use Grav\Common\Config\Config;
|
|
use Grav\Common\Data\Blueprint;
|
|
use Grav\Common\Debugger;
|
|
use Grav\Common\Grav;
|
|
use Grav\Common\Page\Interfaces\PageInterface;
|
|
use Grav\Common\User\Interfaces\UserInterface;
|
|
use Grav\Common\Utils;
|
|
use Grav\Framework\Cache\Adapter\DoctrineCache;
|
|
use Grav\Framework\Cache\Adapter\MemoryCache;
|
|
use Grav\Framework\Cache\CacheInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
|
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
|
use Grav\Framework\Flex\Storage\SimpleStorage;
|
|
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
|
|
use Psr\SimpleCache\InvalidArgumentException;
|
|
use RocketTheme\Toolbox\File\YamlFile;
|
|
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
|
use RuntimeException;
|
|
use function call_user_func_array;
|
|
use function count;
|
|
use function is_array;
|
|
use Grav\Common\Flex\Types\Generic\GenericObject;
|
|
use Grav\Common\Flex\Types\Generic\GenericCollection;
|
|
use Grav\Common\Flex\Types\Generic\GenericIndex;
|
|
use function is_callable;
|
|
|
|
/**
|
|
* Class FlexDirectory
|
|
* @package Grav\Framework\Flex
|
|
* @template T
|
|
*/
|
|
class FlexDirectory implements FlexDirectoryInterface
|
|
{
|
|
use FlexAuthorizeTrait;
|
|
|
|
/** @var string */
|
|
protected $type;
|
|
/** @var string */
|
|
protected $blueprint_file;
|
|
/** @var Blueprint[] */
|
|
protected $blueprints;
|
|
/** @var FlexIndexInterface[] */
|
|
protected $indexes = [];
|
|
/** @var FlexCollectionInterface|null */
|
|
protected $collection;
|
|
/** @var bool */
|
|
protected $enabled;
|
|
/** @var array */
|
|
protected $defaults;
|
|
/** @var Config */
|
|
protected $config;
|
|
/** @var FlexStorageInterface */
|
|
protected $storage;
|
|
/** @var CacheInterface[] */
|
|
protected $cache;
|
|
/** @var FlexObjectInterface[] */
|
|
protected $objects;
|
|
/** @var string */
|
|
protected $objectClassName;
|
|
/** @var string */
|
|
protected $collectionClassName;
|
|
/** @var string */
|
|
protected $indexClassName;
|
|
|
|
/** @var string|null */
|
|
private $_authorize;
|
|
|
|
/**
|
|
* FlexDirectory constructor.
|
|
* @param string $type
|
|
* @param string $blueprint_file
|
|
* @param array $defaults
|
|
*/
|
|
public function __construct(string $type, string $blueprint_file, array $defaults = [])
|
|
{
|
|
$this->type = $type;
|
|
$this->blueprints = [];
|
|
$this->blueprint_file = $blueprint_file;
|
|
$this->defaults = $defaults;
|
|
$this->enabled = !empty($defaults['enabled']);
|
|
$this->objects = [];
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isListed(): bool
|
|
{
|
|
$grav = Grav::instance();
|
|
|
|
/** @var Flex $flex */
|
|
$flex = $grav['flex'];
|
|
$directory = $flex->getDirectory($this->type);
|
|
|
|
return null !== $directory;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isEnabled(): bool
|
|
{
|
|
return $this->enabled;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getFlexType(): string
|
|
{
|
|
return $this->type;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getTitle(): string
|
|
{
|
|
return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType()));
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getDescription(): string
|
|
{
|
|
return $this->getBlueprintInternal()->get('description', '');
|
|
}
|
|
|
|
/**
|
|
* @param string|null $name
|
|
* @param mixed $default
|
|
* @return mixed
|
|
*/
|
|
public function getConfig(string $name = null, $default = null)
|
|
{
|
|
if (null === $this->config) {
|
|
$config = $this->getBlueprintInternal()->get('config', []);
|
|
$config = is_array($config) ? array_replace_recursive($config, $this->defaults, $this->getDirectoryConfig($config['admin']['views']['configure']['form'] ?? $config['admin']['configure']['form'] ?? null)) : null;
|
|
if (!is_array($config)) {
|
|
throw new RuntimeException('Bad configuration');
|
|
}
|
|
|
|
$this->config = new Config($config);
|
|
}
|
|
|
|
return null === $name ? $this->config : $this->config->get($name, $default);
|
|
}
|
|
|
|
/**
|
|
* @param string|null $name
|
|
* @param array $options
|
|
* @return FlexFormInterface
|
|
* @internal
|
|
*/
|
|
public function getDirectoryForm(string $name = null, array $options = [])
|
|
{
|
|
$name = $name ?: $this->getConfig('admin.views.configure.form', '') ?: $this->getConfig('admin.configure.form', '');
|
|
|
|
return new FlexDirectoryForm($name ?? '', $this, $options);
|
|
}
|
|
|
|
/**
|
|
* @return Blueprint
|
|
* @internal
|
|
*/
|
|
public function getDirectoryBlueprint()
|
|
{
|
|
$name = 'configure';
|
|
|
|
$type = $this->getBlueprint();
|
|
$overrides = $type->get("blueprints/{$name}");
|
|
|
|
$path = "blueprints://flex/shared/{$name}.yaml";
|
|
$blueprint = new Blueprint($path);
|
|
$blueprint->load();
|
|
if (isset($overrides['fields'])) {
|
|
$blueprint->embed('form/fields/tabs/fields', $overrides['fields']);
|
|
}
|
|
$blueprint->init();
|
|
|
|
return $blueprint;
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @param array $data
|
|
* @return void
|
|
* @throws Exception
|
|
* @internal
|
|
*/
|
|
public function saveDirectoryConfig(string $name, array $data)
|
|
{
|
|
$grav = Grav::instance();
|
|
|
|
/** @var UniformResourceLocator $locator */
|
|
$locator = $grav['locator'];
|
|
/** @var string $filename Filename is always string */
|
|
$filename = $locator->findResource($this->getDirectoryConfigUri($name), true, true);
|
|
|
|
$file = YamlFile::instance($filename);
|
|
if (!empty($data)) {
|
|
$file->save($data);
|
|
} else {
|
|
$file->delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @return array
|
|
* @internal
|
|
*/
|
|
public function loadDirectoryConfig(string $name): array
|
|
{
|
|
$grav = Grav::instance();
|
|
|
|
/** @var UniformResourceLocator $locator */
|
|
$locator = $grav['locator'];
|
|
$uri = $this->getDirectoryConfigUri($name);
|
|
|
|
// If configuration is found in main configuration, use it.
|
|
if (str_starts_with($uri, 'config://')) {
|
|
$path = str_replace('/', '.', substr($uri, 9, -5));
|
|
|
|
return (array)$grav['config']->get($path);
|
|
}
|
|
|
|
// Load the configuration file.
|
|
$filename = $locator->findResource($uri, true);
|
|
if ($filename === false) {
|
|
return [];
|
|
}
|
|
|
|
$file = YamlFile::instance($filename);
|
|
|
|
return $file->content();
|
|
}
|
|
|
|
/**
|
|
* @param string|null $name
|
|
* @return string
|
|
*/
|
|
public function getDirectoryConfigUri(string $name = null): string
|
|
{
|
|
$name = $name ?: $this->getFlexType();
|
|
$blueprint = $this->getBlueprint();
|
|
|
|
return $blueprint->get('blueprints/views/configure/file') ?? $blueprint->get('blueprints/configure/file') ?? "config://flex/{$name}.yaml";
|
|
}
|
|
|
|
/**
|
|
* @param string|null $name
|
|
* @return array
|
|
*/
|
|
protected function getDirectoryConfig(string $name = null): array
|
|
{
|
|
$grav = Grav::instance();
|
|
|
|
/** @var Config $config */
|
|
$config = $grav['config'];
|
|
$name = $name ?: $this->getFlexType();
|
|
|
|
return $config->get("flex.{$name}", []);
|
|
}
|
|
|
|
/**
|
|
* Returns a new uninitialized instance of blueprint.
|
|
*
|
|
* Always use $object->getBlueprint() or $object->getForm()->getBlueprint() instead.
|
|
*
|
|
* @param string $type
|
|
* @param string $context
|
|
* @return Blueprint
|
|
*/
|
|
public function getBlueprint(string $type = '', string $context = '')
|
|
{
|
|
return clone $this->getBlueprintInternal($type, $context);
|
|
}
|
|
|
|
/**
|
|
* @param string $view
|
|
* @return string
|
|
*/
|
|
public function getBlueprintFile(string $view = ''): string
|
|
{
|
|
$file = $this->blueprint_file;
|
|
if ($view !== '') {
|
|
$file = preg_replace('/\.yaml/', "/{$view}.yaml", $file);
|
|
}
|
|
|
|
return (string)$file;
|
|
}
|
|
|
|
/**
|
|
* Get collection. In the site this will be filtered by the default filters (published etc).
|
|
*
|
|
* Use $directory->getIndex() if you want unfiltered collection.
|
|
*
|
|
* @param array|null $keys Array of keys.
|
|
* @param string|null $keyField Field to be used as the key.
|
|
* @return FlexCollectionInterface
|
|
* @phpstan-return FlexCollectionInterface<T>
|
|
*/
|
|
public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface
|
|
{
|
|
// Get all selected entries.
|
|
$index = $this->getIndex($keys, $keyField);
|
|
|
|
if (!Utils::isAdminPlugin()) {
|
|
// If not in admin, filter the list by using default filters.
|
|
$filters = (array)$this->getConfig('site.filter', []);
|
|
|
|
foreach ($filters as $filter) {
|
|
$index = $index->{$filter}();
|
|
}
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* Get the full collection of all stored objects.
|
|
*
|
|
* Use $directory->getCollection() if you want a filtered collection.
|
|
*
|
|
* @param array|null $keys Array of keys.
|
|
* @param string|null $keyField Field to be used as the key.
|
|
* @return FlexIndexInterface
|
|
*/
|
|
public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface
|
|
{
|
|
$keyField = $keyField ?? '';
|
|
$index = $this->indexes[$keyField] ?? $this->loadIndex($keyField);
|
|
$index = clone $index;
|
|
|
|
if (null !== $keys) {
|
|
/** @var FlexIndexInterface $index */
|
|
$index = $index->select($keys);
|
|
}
|
|
|
|
return $index->getIndex();
|
|
}
|
|
|
|
/**
|
|
* Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object.
|
|
*
|
|
* Note: It is not safe to use the object without checking if the user can access it.
|
|
*
|
|
* @param string|null $key
|
|
* @param string|null $keyField Field to be used as the key.
|
|
* @return FlexObjectInterface|null
|
|
*/
|
|
public function getObject($key = null, string $keyField = null): ?FlexObjectInterface
|
|
{
|
|
if (null === $key) {
|
|
return $this->createObject([], '');
|
|
}
|
|
|
|
$keyField = $keyField ?? '';
|
|
$index = $this->indexes[$keyField] ?? $this->loadIndex($keyField);
|
|
|
|
return $index->get($key);
|
|
}
|
|
|
|
/**
|
|
* @param string|null $namespace
|
|
* @return CacheInterface
|
|
*/
|
|
public function getCache(string $namespace = null)
|
|
{
|
|
$namespace = $namespace ?: 'index';
|
|
$cache = $this->cache[$namespace] ?? null;
|
|
|
|
if (null === $cache) {
|
|
try {
|
|
$grav = Grav::instance();
|
|
|
|
/** @var Cache $gravCache */
|
|
$gravCache = $grav['cache'];
|
|
$config = $this->getConfig('object.cache.' . $namespace);
|
|
if (empty($config['enabled'])) {
|
|
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
|
|
} else {
|
|
$lifetime = $config['lifetime'] ?? 60;
|
|
|
|
$key = $gravCache->getKey();
|
|
if (Utils::isAdminPlugin()) {
|
|
$key = substr($key, 0, -1);
|
|
}
|
|
$cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $lifetime);
|
|
}
|
|
} catch (Exception $e) {
|
|
/** @var Debugger $debugger */
|
|
$debugger = Grav::instance()['debugger'];
|
|
$debugger->addException($e);
|
|
|
|
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
|
|
}
|
|
|
|
// Disable cache key validation.
|
|
$cache->setValidation(false);
|
|
$this->cache[$namespace] = $cache;
|
|
}
|
|
|
|
return $cache;
|
|
}
|
|
|
|
/**
|
|
* @return $this
|
|
*/
|
|
public function clearCache()
|
|
{
|
|
$grav = Grav::instance();
|
|
|
|
/** @var Debugger $debugger */
|
|
$debugger = $grav['debugger'];
|
|
$debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug');
|
|
|
|
/** @var UniformResourceLocator $locator */
|
|
$locator = $grav['locator'];
|
|
$locator->clearCache();
|
|
|
|
$this->getCache('index')->clear();
|
|
$this->getCache('object')->clear();
|
|
$this->getCache('render')->clear();
|
|
|
|
$this->indexes = [];
|
|
$this->objects = [];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string|null $key
|
|
* @return string|null
|
|
*/
|
|
public function getStorageFolder(string $key = null): ?string
|
|
{
|
|
return $this->getStorage()->getStoragePath($key);
|
|
}
|
|
|
|
/**
|
|
* @param string|null $key
|
|
* @return string|null
|
|
*/
|
|
public function getMediaFolder(string $key = null): ?string
|
|
{
|
|
return $this->getStorage()->getMediaPath($key);
|
|
}
|
|
|
|
/**
|
|
* @return FlexStorageInterface
|
|
*/
|
|
public function getStorage(): FlexStorageInterface
|
|
{
|
|
if (null === $this->storage) {
|
|
$this->storage = $this->createStorage();
|
|
}
|
|
|
|
return $this->storage;
|
|
}
|
|
|
|
/**
|
|
* @param array $data
|
|
* @param string $key
|
|
* @param bool $validate
|
|
* @return FlexObjectInterface
|
|
*/
|
|
public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface
|
|
{
|
|
/** @var string|FlexObjectInterface $className */
|
|
$className = $this->objectClassName ?: $this->getObjectClass();
|
|
|
|
return new $className($data, $key, $this, $validate);
|
|
}
|
|
|
|
/**
|
|
* @param array $entries
|
|
* @param string|null $keyField
|
|
* @return FlexCollectionInterface
|
|
*/
|
|
public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface
|
|
{
|
|
/** @var string|FlexCollectionInterface $className */
|
|
$className = $this->collectionClassName ?: $this->getCollectionClass();
|
|
|
|
return $className::createFromArray($entries, $this, $keyField);
|
|
}
|
|
|
|
/**
|
|
* @param array $entries
|
|
* @param string|null $keyField
|
|
* @return FlexIndexInterface
|
|
*/
|
|
public function createIndex(array $entries, string $keyField = null): FlexIndexInterface
|
|
{
|
|
/** @var string|FlexIndexInterface $className */
|
|
$className = $this->indexClassName ?: $this->getIndexClass();
|
|
|
|
return $className::createFromArray($entries, $this, $keyField);
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getObjectClass(): string
|
|
{
|
|
if (!$this->objectClassName) {
|
|
$this->objectClassName = $this->getConfig('data.object', GenericObject::class);
|
|
}
|
|
|
|
return $this->objectClassName;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getCollectionClass(): string
|
|
{
|
|
if (!$this->collectionClassName) {
|
|
$this->collectionClassName = $this->getConfig('data.collection', GenericCollection::class);
|
|
}
|
|
|
|
return $this->collectionClassName;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getIndexClass(): string
|
|
{
|
|
if (!$this->indexClassName) {
|
|
$this->indexClassName = $this->getConfig('data.index', GenericIndex::class);
|
|
}
|
|
|
|
return $this->indexClassName;
|
|
}
|
|
|
|
/**
|
|
* @param array $entries
|
|
* @param string|null $keyField
|
|
* @return FlexCollectionInterface
|
|
*/
|
|
public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface
|
|
{
|
|
return $this->createCollection($this->loadObjects($entries), $keyField);
|
|
}
|
|
|
|
/**
|
|
* @param array $entries
|
|
* @return FlexObjectInterface[]
|
|
* @internal
|
|
*/
|
|
public function loadObjects(array $entries): array
|
|
{
|
|
/** @var Debugger $debugger */
|
|
$debugger = Grav::instance()['debugger'];
|
|
|
|
$keys = [];
|
|
$rows = [];
|
|
$fetch = [];
|
|
|
|
// Build lookup arrays with storage keys for the objects.
|
|
foreach ($entries as $key => $value) {
|
|
$k = $value['storage_key'] ?? '';
|
|
if ($k === '') {
|
|
continue;
|
|
}
|
|
$v = $this->objects[$k] ?? null;
|
|
$keys[$k] = $key;
|
|
$rows[$k] = $v;
|
|
if (!$v) {
|
|
$fetch[] = $k;
|
|
}
|
|
}
|
|
|
|
// Attempt to fetch missing rows from the cache.
|
|
if ($fetch) {
|
|
$rows = (array)array_replace($rows, $this->loadCachedObjects($fetch));
|
|
}
|
|
|
|
// Read missing rows from the storage.
|
|
$updated = [];
|
|
$storage = $this->getStorage();
|
|
$rows = $storage->readRows($rows, $updated);
|
|
|
|
// Create objects from the rows.
|
|
$isListed = $this->isListed();
|
|
$list = [];
|
|
foreach ($rows as $storageKey => $row) {
|
|
$usedKey = $keys[$storageKey];
|
|
|
|
if ($row instanceof FlexObjectInterface) {
|
|
$object = $row;
|
|
} else {
|
|
if ($row === null) {
|
|
$debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
|
|
continue;
|
|
}
|
|
|
|
if (isset($row['__ERROR'])) {
|
|
$message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__ERROR']);
|
|
$debugger->addException(new RuntimeException($message));
|
|
$debugger->addMessage($message, 'error');
|
|
continue;
|
|
}
|
|
|
|
if (!isset($row['__META'])) {
|
|
$row['__META'] = [
|
|
'storage_key' => $storageKey,
|
|
'storage_timestamp' => $entries[$usedKey]['storage_timestamp'] ?? 0,
|
|
];
|
|
}
|
|
|
|
$key = $row['__META']['key'] ?? $entries[$usedKey]['key'] ?? $usedKey;
|
|
$object = $this->createObject($row, $key, false);
|
|
$this->objects[$storageKey] = $object;
|
|
if ($isListed) {
|
|
// If unserialize works for the object, serialize the object to speed up the loading.
|
|
$updated[$storageKey] = $object;
|
|
}
|
|
}
|
|
|
|
$list[$usedKey] = $object;
|
|
}
|
|
|
|
// Store updated rows to the cache.
|
|
if ($updated) {
|
|
$cache = $this->getCache('object');
|
|
if (!$cache instanceof MemoryCache) {
|
|
///** @var Debugger $debugger */
|
|
//$debugger = Grav::instance()['debugger'];
|
|
//$debugger->addMessage(sprintf('Flex: Caching %d %s', \count($entries), $this->type), 'debug');
|
|
}
|
|
try {
|
|
$cache->setMultiple($updated);
|
|
} catch (InvalidArgumentException $e) {
|
|
$debugger->addException($e);
|
|
// TODO: log about the issue.
|
|
}
|
|
}
|
|
|
|
if ($fetch) {
|
|
$debugger->stopTimer('flex-objects');
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
protected function loadCachedObjects(array $fetch): array
|
|
{
|
|
if (!$fetch) {
|
|
return [];
|
|
}
|
|
|
|
/** @var Debugger $debugger */
|
|
$debugger = Grav::instance()['debugger'];
|
|
|
|
$cache = $this->getCache('object');
|
|
|
|
// Attempt to fetch missing rows from the cache.
|
|
$fetched = [];
|
|
try {
|
|
$loading = count($fetch);
|
|
|
|
$debugger->startTimer('flex-objects', sprintf('Flex: Loading %d %s', $loading, $this->type));
|
|
|
|
$fetched = (array)$cache->getMultiple($fetch);
|
|
if ($fetched) {
|
|
$index = $this->loadIndex('storage_key');
|
|
|
|
// Make sure cached objects are up to date: compare against index checksum/timestamp.
|
|
/**
|
|
* @var string $key
|
|
* @var mixed $value
|
|
*/
|
|
foreach ($fetched as $key => $value) {
|
|
if ($value instanceof FlexObjectInterface) {
|
|
$objectMeta = $value->getMetaData();
|
|
} else {
|
|
$objectMeta = $value['__META'] ?? [];
|
|
}
|
|
$indexMeta = $index->getMetaData($key);
|
|
|
|
$indexChecksum = $indexMeta['checksum'] ?? $indexMeta['storage_timestamp'] ?? null;
|
|
$objectChecksum = $objectMeta['checksum'] ?? $objectMeta['storage_timestamp'] ?? null;
|
|
if ($indexChecksum !== $objectChecksum) {
|
|
unset($fetched[$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (InvalidArgumentException $e) {
|
|
$debugger->addException($e);
|
|
}
|
|
|
|
return $fetched;
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function reloadIndex(): void
|
|
{
|
|
$this->getCache('index')->clear();
|
|
|
|
$this->indexes = [];
|
|
$this->objects = [];
|
|
}
|
|
|
|
/**
|
|
* @param string $scope
|
|
* @param string $action
|
|
* @return string
|
|
*/
|
|
public function getAuthorizeRule(string $scope, string $action): string
|
|
{
|
|
if (!$this->_authorize) {
|
|
$config = $this->getConfig('admin.permissions');
|
|
if ($config) {
|
|
$this->_authorize = array_key_first($config) . '.%2$s';
|
|
} else {
|
|
$this->_authorize = '%1$s.flex-object.%2$s';
|
|
}
|
|
}
|
|
|
|
return sprintf($this->_authorize, $scope, $action);
|
|
}
|
|
|
|
/**
|
|
* @param string $type_view
|
|
* @param string $context
|
|
* @return Blueprint
|
|
*/
|
|
protected function getBlueprintInternal(string $type_view = '', string $context = '')
|
|
{
|
|
if (!isset($this->blueprints[$type_view])) {
|
|
if (!file_exists($this->blueprint_file)) {
|
|
throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type));
|
|
}
|
|
|
|
$parts = explode('.', rtrim($type_view, '.'), 2);
|
|
$type = array_shift($parts);
|
|
$view = array_shift($parts) ?: '';
|
|
|
|
$blueprint = new Blueprint($this->getBlueprintFile($view));
|
|
$blueprint->addDynamicHandler('data', function (array &$field, $property, array &$call) {
|
|
$this->dynamicDataField($field, $property, $call);
|
|
});
|
|
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
|
|
$this->dynamicFlexField($field, $property, $call);
|
|
});
|
|
|
|
if ($context) {
|
|
$blueprint->setContext($context);
|
|
}
|
|
|
|
$blueprint->load($type ?: null);
|
|
if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) {
|
|
$blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load();
|
|
$blueprint->extend($blueprintBase, true);
|
|
}
|
|
|
|
$this->blueprints[$type_view] = $blueprint;
|
|
}
|
|
|
|
return $this->blueprints[$type_view];
|
|
}
|
|
|
|
/**
|
|
* @param array $field
|
|
* @param string $property
|
|
* @param array $call
|
|
* @return void
|
|
*/
|
|
protected function dynamicDataField(array &$field, $property, array $call)
|
|
{
|
|
$params = $call['params'];
|
|
if (is_array($params)) {
|
|
$function = array_shift($params);
|
|
} else {
|
|
$function = $params;
|
|
$params = [];
|
|
}
|
|
|
|
$object = $call['object'];
|
|
if ($function === '\Grav\Common\Page\Pages::pageTypes') {
|
|
$params = [$object instanceof PageInterface && $object->isModule() ? 'modular' : 'standard'];
|
|
}
|
|
|
|
$data = null;
|
|
if (is_callable($function)) {
|
|
$data = call_user_func_array($function, $params);
|
|
}
|
|
|
|
// If function returns a value,
|
|
if (null !== $data) {
|
|
if (is_array($data) && isset($field[$property]) && is_array($field[$property])) {
|
|
// Combine field and @data-field together.
|
|
$field[$property] += $data;
|
|
} else {
|
|
// Or create/replace field with @data-field.
|
|
$field[$property] = $data;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $field
|
|
* @param string $property
|
|
* @param array $call
|
|
* @return void
|
|
*/
|
|
protected function dynamicFlexField(array &$field, $property, array $call): void
|
|
{
|
|
$params = (array)$call['params'];
|
|
$object = $call['object'] ?? null;
|
|
$method = array_shift($params);
|
|
$not = false;
|
|
if (str_starts_with($method, '!')) {
|
|
$method = substr($method, 1);
|
|
$not = true;
|
|
} elseif (str_starts_with($method, 'not ')) {
|
|
$method = substr($method, 4);
|
|
$not = true;
|
|
}
|
|
$method = trim($method);
|
|
|
|
if ($object && method_exists($object, $method)) {
|
|
$value = $object->{$method}(...$params);
|
|
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
|
|
$value = $this->mergeArrays($field[$property], $value);
|
|
}
|
|
$field[$property] = $not ? !$value : $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $array1
|
|
* @param array $array2
|
|
* @return array
|
|
*/
|
|
protected function mergeArrays(array $array1, array $array2): array
|
|
{
|
|
foreach ($array2 as $key => $value) {
|
|
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
|
|
$array1[$key] = $this->mergeArrays($array1[$key], $value);
|
|
} else {
|
|
$array1[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $array1;
|
|
}
|
|
|
|
/**
|
|
* @return FlexStorageInterface
|
|
*/
|
|
protected function createStorage(): FlexStorageInterface
|
|
{
|
|
$this->collection = $this->createCollection([]);
|
|
|
|
$storage = $this->getConfig('data.storage');
|
|
|
|
if (!is_array($storage)) {
|
|
$storage = ['options' => ['folder' => $storage]];
|
|
}
|
|
|
|
$className = $storage['class'] ?? SimpleStorage::class;
|
|
$options = $storage['options'] ?? [];
|
|
|
|
return new $className($options);
|
|
}
|
|
|
|
/**
|
|
* @param string $keyField
|
|
* @return FlexIndexInterface
|
|
*/
|
|
protected function loadIndex(string $keyField): FlexIndexInterface
|
|
{
|
|
static $i = 0;
|
|
|
|
$index = $this->indexes[$keyField] ?? null;
|
|
if (null !== $index) {
|
|
return $index;
|
|
}
|
|
|
|
$index = $this->indexes['storage_key'] ?? null;
|
|
if (null === $index) {
|
|
$i++;
|
|
$j = $i;
|
|
/** @var Debugger $debugger */
|
|
$debugger = Grav::instance()['debugger'];
|
|
$debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index");
|
|
|
|
$storage = $this->getStorage();
|
|
$cache = $this->getCache('index');
|
|
|
|
try {
|
|
$keys = $cache->get('__keys');
|
|
} catch (InvalidArgumentException $e) {
|
|
$debugger->addException($e);
|
|
$keys = null;
|
|
}
|
|
|
|
if (!is_array($keys)) {
|
|
/** @var string|FlexIndexInterface $className */
|
|
$className = $this->getIndexClass();
|
|
$keys = $className::loadEntriesFromStorage($storage);
|
|
if (!$cache instanceof MemoryCache) {
|
|
$debugger->addMessage(
|
|
sprintf('Flex: Caching %s index of %d objects', $this->type, count($keys)),
|
|
'debug'
|
|
);
|
|
}
|
|
try {
|
|
$cache->set('__keys', $keys);
|
|
} catch (InvalidArgumentException $e) {
|
|
$debugger->addException($e);
|
|
// TODO: log about the issue.
|
|
}
|
|
}
|
|
|
|
$ordering = $this->getConfig('data.ordering', []);
|
|
|
|
// We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop.
|
|
$this->indexes['storage_key'] = $index = $this->createIndex($keys, 'storage_key');
|
|
if ($ordering) {
|
|
/** @var FlexCollectionInterface $collection */
|
|
$collection = $this->indexes['storage_key']->orderBy($ordering);
|
|
$this->indexes['storage_key'] = $index = $collection->getIndex();
|
|
}
|
|
|
|
$debugger->stopTimer('flex-keys-' . $this->type . $j);
|
|
}
|
|
|
|
if ($keyField !== 'storage_key') {
|
|
$this->indexes[$keyField] = $index = $index->withKeyField($keyField ?: null);
|
|
}
|
|
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* @param string $action
|
|
* @return string
|
|
*/
|
|
protected function getAuthorizeAction(string $action): string
|
|
{
|
|
// Handle special action save, which can mean either update or create.
|
|
if ($action === 'save') {
|
|
$action = 'create';
|
|
}
|
|
|
|
return $action;
|
|
}
|
|
/**
|
|
* @return UserInterface|null
|
|
*/
|
|
protected function getActiveUser(): ?UserInterface
|
|
{
|
|
/** @var UserInterface|null $user */
|
|
$user = Grav::instance()['user'] ?? null;
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
protected function getAuthorizeScope(): string
|
|
{
|
|
return isset(Grav::instance()['admin']) ? 'admin' : 'site';
|
|
}
|
|
|
|
// DEPRECATED METHODS
|
|
|
|
/**
|
|
* @return string
|
|
* @deprecated 1.6 Use ->getFlexType() method instead.
|
|
*/
|
|
public function getType(): string
|
|
{
|
|
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
|
|
|
|
return $this->type;
|
|
}
|
|
|
|
/**
|
|
* @param array $data
|
|
* @param string|null $key
|
|
* @return FlexObjectInterface
|
|
* @deprecated 1.7 Use $object->update()->save() instead.
|
|
*/
|
|
public function update(array $data, string $key = null): FlexObjectInterface
|
|
{
|
|
user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->update()->save() instead.', E_USER_DEPRECATED);
|
|
|
|
$object = null !== $key ? $this->getIndex()->get($key): null;
|
|
|
|
$storage = $this->getStorage();
|
|
|
|
if (null === $object) {
|
|
$object = $this->createObject($data, $key ?? '', true);
|
|
$key = $object->getStorageKey();
|
|
|
|
if ($key) {
|
|
$storage->replaceRows([$key => $object->prepareStorage()]);
|
|
} else {
|
|
$storage->createRows([$object->prepareStorage()]);
|
|
}
|
|
} else {
|
|
$oldKey = $object->getStorageKey();
|
|
$object->update($data);
|
|
$newKey = $object->getStorageKey();
|
|
|
|
if ($oldKey !== $newKey) {
|
|
$object->triggerEvent('move');
|
|
$storage->renameRow($oldKey, $newKey);
|
|
// TODO: media support.
|
|
}
|
|
|
|
$object->save();
|
|
}
|
|
|
|
try {
|
|
$this->clearCache();
|
|
} catch (InvalidArgumentException $e) {
|
|
/** @var Debugger $debugger */
|
|
$debugger = Grav::instance()['debugger'];
|
|
$debugger->addException($e);
|
|
|
|
// Caching failed, but we can ignore that for now.
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* @param string $key
|
|
* @return FlexObjectInterface|null
|
|
* @deprecated 1.7 Use $object->delete() instead.
|
|
*/
|
|
public function remove(string $key): ?FlexObjectInterface
|
|
{
|
|
user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->delete() instead.', E_USER_DEPRECATED);
|
|
|
|
$object = $this->getIndex()->get($key);
|
|
if (!$object) {
|
|
return null;
|
|
}
|
|
|
|
$object->delete();
|
|
|
|
return $object;
|
|
}
|
|
}
|