maj grav
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Object\ObjectCollection;
|
||||
|
||||
/**
|
||||
* Class Flex
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class Flex implements \Countable
|
||||
{
|
||||
/** @var array */
|
||||
protected $config;
|
||||
|
||||
/** @var FlexDirectory[] */
|
||||
protected $types;
|
||||
|
||||
/**
|
||||
* Flex constructor.
|
||||
* @param array $types List of [type => blueprint file, ...]
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $types, array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->types = [];
|
||||
|
||||
foreach ($types as $type => $blueprint) {
|
||||
$this->addDirectoryType($type, $blueprint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $blueprint
|
||||
* @param array $config
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectoryType(string $type, string $blueprint, array $config = [])
|
||||
{
|
||||
$config = array_merge_recursive(['enabled' => true], $this->config['object'] ?? [], $config);
|
||||
|
||||
$this->types[$type] = new FlexDirectory($type, $blueprint, $config);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectory(FlexDirectory $directory)
|
||||
{
|
||||
$this->types[$directory->getFlexType()] = $directory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDirectory(string $type): bool
|
||||
{
|
||||
return isset($this->types[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string[] $types
|
||||
* @param bool $keepMissing
|
||||
* @return array|FlexDirectory[]
|
||||
*/
|
||||
public function getDirectories(array $types = null, bool $keepMissing = false): array
|
||||
{
|
||||
if ($types === null) {
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
// Return the directories in the given order.
|
||||
$directories = [];
|
||||
foreach ($types as $type) {
|
||||
$directories[$type] = $this->types[$type] ?? null;
|
||||
}
|
||||
|
||||
return $keepMissing ? $directories : array_filter($directories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory|null
|
||||
*/
|
||||
public function getDirectory(string $type): ?FlexDirectory
|
||||
{
|
||||
return $this->types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param array|null $keys
|
||||
* @param string|null $keyField
|
||||
* @return FlexCollectionInterface|null
|
||||
*/
|
||||
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
|
||||
{
|
||||
$directory = $type ? $this->getDirectory($type) : null;
|
||||
|
||||
return $directory ? $directory->getCollection($keys, $keyField) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options In addition to the options in getObjects(), following options can be passed:
|
||||
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
|
||||
* @return FlexCollectionInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
|
||||
{
|
||||
$collectionClass = $options['collection_class'] ?? ObjectCollection::class;
|
||||
if (!class_exists($collectionClass)) {
|
||||
throw new \RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass));
|
||||
}
|
||||
|
||||
$objects = $this->getObjects($keys, $options);
|
||||
|
||||
return new $collectionClass($objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options Following optional options can be passed:
|
||||
* types: List of allowed types.
|
||||
* type: Allowed type if types isn't defined, otherwise acts as default_type.
|
||||
* default_type: Set default type for objects given without type (only used if key_field isn't set).
|
||||
* keep_missing: Set to true if you want to return missing objects as null.
|
||||
* key_field: Key field which is used to match the objects.
|
||||
* @return array
|
||||
*/
|
||||
public function getObjects(array $keys, array $options = []): array
|
||||
{
|
||||
$type = $options['type'] ?? null;
|
||||
$defaultType = $options['default_type'] ?? $type ?? null;
|
||||
$keyField = $options['key_field'] ?? 'flex_key';
|
||||
|
||||
// Prepare empty result lists for all requested Flex types.
|
||||
$types = $options['types'] ?? (array)$type ?: null;
|
||||
if ($types) {
|
||||
$types = array_fill_keys($types, []);
|
||||
}
|
||||
$strict = isset($types);
|
||||
|
||||
$guessed = [];
|
||||
if ($keyField === 'flex_key') {
|
||||
// We need to split Flex key lookups into individual directories.
|
||||
$undefined = [];
|
||||
$keyFieldFind = 'storage_key';
|
||||
|
||||
foreach ($keys as $flexKey) {
|
||||
if (!$flexKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$flexKey = (string)$flexKey;
|
||||
// Normalize key and type using fallback to default type if it was set.
|
||||
[$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType);
|
||||
|
||||
if ($type === '' && $types) {
|
||||
// Add keys which are not associated to any Flex type. They will be included to every Flex type.
|
||||
foreach ($types as $type => &$array) {
|
||||
$array[] = $key;
|
||||
$guessed[$key][] = "{$type}.obj:{$key}";
|
||||
}
|
||||
unset($array);
|
||||
} elseif (!$strict || isset($types[$type])) {
|
||||
// Collect keys by their Flex type. If allowed types are defined, only include values from those types.
|
||||
$types[$type][] = $key;
|
||||
if ($guess) {
|
||||
$guessed[$key][] = "{$type}.obj:{$key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We are using a specific key field, make every key undefined.
|
||||
$undefined = $keys;
|
||||
$keyFieldFind = $keyField;
|
||||
}
|
||||
|
||||
if (!$types) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [[]];
|
||||
foreach ($types as $type => $typeKeys) {
|
||||
// Also remember to look up keys from undefined Flex types.
|
||||
$lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys;
|
||||
|
||||
$collection = $this->getCollection($type, $lookupKeys, $keyFieldFind);
|
||||
if ($collection && $keyFieldFind !== $keyField) {
|
||||
$collection = $collection->withKeyField($keyField);
|
||||
}
|
||||
|
||||
$list[] = $collection ? $collection->toArray() : [];
|
||||
}
|
||||
|
||||
// Merge objects from individual types back together.
|
||||
$list = array_merge(...$list);
|
||||
|
||||
// Use the original key ordering.
|
||||
if (!$guessed) {
|
||||
$list = array_replace(array_fill_keys($keys, null), $list);
|
||||
} else {
|
||||
// We have mixed keys, we need to map flex keys back to storage keys.
|
||||
$results = [];
|
||||
foreach ($keys as $key) {
|
||||
$flexKey = $guessed[$key] ?? $key;
|
||||
if (\is_array($flexKey)) {
|
||||
$result = null;
|
||||
foreach ($flexKey as $tryKey) {
|
||||
if ($result = $list[$tryKey] ?? null) {
|
||||
// Use the first matching object (conflicting objects will be ignored for now).
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = $list[$flexKey] ?? null;
|
||||
}
|
||||
|
||||
$results[$key] = $result;
|
||||
}
|
||||
|
||||
$list = $results;
|
||||
}
|
||||
|
||||
// Remove missing objects if not asked to keep them.
|
||||
if (empty($option['keep_missing'])) {
|
||||
$list = array_filter($list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string|null $type
|
||||
* @param string|null $keyField
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
|
||||
{
|
||||
if (null === $type && null === $keyField) {
|
||||
// Special handling for quick Flex key lookups.
|
||||
$keyField = 'storage_key';
|
||||
[$type, $key] = $this->resolveKeyAndType($key, $type);
|
||||
} else {
|
||||
$type = $this->resolveType($type);
|
||||
}
|
||||
|
||||
if ($type === '' || $key === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$directory = $this->getDirectory($type);
|
||||
|
||||
return $directory ? $directory->getObject($key, $keyField) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->types);
|
||||
}
|
||||
|
||||
protected function resolveKeyAndType(string $flexKey, string $type = null): array
|
||||
{
|
||||
$guess = false;
|
||||
if (strpos($flexKey, ':') !== false) {
|
||||
[$type, $key] = explode(':', $flexKey, 2);
|
||||
|
||||
$type = $this->resolveType($type);
|
||||
} else {
|
||||
$key = $flexKey;
|
||||
$type = (string)$type;
|
||||
$guess = true;
|
||||
}
|
||||
|
||||
return [$key, $type, $guess];
|
||||
}
|
||||
|
||||
protected function resolveType(string $type = null): string
|
||||
{
|
||||
if (null !== $type && strpos($type, '.') !== false) {
|
||||
return preg_replace('|\.obj$|', '', $type);
|
||||
}
|
||||
|
||||
return $type ?? '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
<?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 Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\ContentBlock\ContentBlockInterface;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Object\ObjectCollection;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/**
|
||||
* Class FlexCollection
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexCollection extends ObjectCollection implements FlexCollectionInterface
|
||||
{
|
||||
/** @var FlexDirectory */
|
||||
private $_flexDirectory;
|
||||
|
||||
/** @var string */
|
||||
private $_keyField;
|
||||
|
||||
/**
|
||||
* Get list of cached methods.
|
||||
*
|
||||
* @return array Returns a list of methods with their caching information.
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'getTypePrefix' => true,
|
||||
'getType' => true,
|
||||
'getFlexDirectory' => true,
|
||||
'getCacheKey' => true,
|
||||
'getCacheChecksum' => true,
|
||||
'getTimestamp' => true,
|
||||
'hasProperty' => true,
|
||||
'getProperty' => true,
|
||||
'hasNestedProperty' => true,
|
||||
'getNestedProperty' => true,
|
||||
'orderBy' => true,
|
||||
|
||||
'render' => false,
|
||||
'isAuthorized' => 'session',
|
||||
'search' => true,
|
||||
'sort' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::createFromArray()
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
|
||||
{
|
||||
$instance = new static($entries, $directory);
|
||||
$instance->setKeyField($keyField);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null)
|
||||
{
|
||||
parent::__construct($entries);
|
||||
|
||||
if ($directory) {
|
||||
$this->setFlexDirectory($directory)->setKey($directory->getFlexType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::search()
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null)
|
||||
{
|
||||
$matching = $this->call('search', [$search, $properties, $options]);
|
||||
$matching = array_filter($matching);
|
||||
|
||||
if ($matching) {
|
||||
uksort($matching, function ($a, $b) {
|
||||
return -($a <=> $b);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->select(array_keys($matching));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::sort()
|
||||
*/
|
||||
public function sort(array $order)
|
||||
{
|
||||
$criteria = Criteria::create()->orderBy($order);
|
||||
|
||||
/** @var FlexCollectionInterface $matching */
|
||||
$matching = $this->matching($criteria);
|
||||
|
||||
return $matching;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filters
|
||||
* @return FlexCollectionInterface|Collection
|
||||
*/
|
||||
public function filterBy(array $filters)
|
||||
{
|
||||
$expr = Criteria::expr();
|
||||
$criteria = Criteria::create();
|
||||
|
||||
foreach ($filters as $key => $value) {
|
||||
$criteria->andWhere($expr->eq($key, $value));
|
||||
}
|
||||
|
||||
return $this->matching($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexType()
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->_flexDirectory->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory
|
||||
{
|
||||
return $this->_flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamp()
|
||||
*/
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
$timestamps = $this->getTimestamps();
|
||||
|
||||
return $timestamps ? max($timestamps) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->call('getKey')));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getCacheChecksum(): string
|
||||
{
|
||||
return sha1(json_encode($this->getTimestamps()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getTimestamps(): array
|
||||
{
|
||||
/** @var int[] $timestamps */
|
||||
$timestamps = $this->call('getTimestamp');
|
||||
|
||||
return $timestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getStorageKeys(): array
|
||||
{
|
||||
/** @var string[] $keys */
|
||||
$keys = $this->call('getStorageKey');
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexKeys(): array
|
||||
{
|
||||
/** @var string[] $keys */
|
||||
$keys = $this->call('getFlexKey');
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::withKeyField()
|
||||
*/
|
||||
public function withKeyField(string $keyField = null)
|
||||
{
|
||||
$keyField = $keyField ?: 'key';
|
||||
if ($keyField === $this->getKeyField()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
// TODO: remove hardcoded logic
|
||||
if ($keyField === 'storage_key') {
|
||||
$entries[$object->getStorageKey()] = $object;
|
||||
} elseif ($keyField === 'flex_key') {
|
||||
$entries[$object->getFlexKey()] = $object;
|
||||
} elseif ($keyField === 'key') {
|
||||
$entries[$object->getKey()] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries, $keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getIndex()
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::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-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $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 ? $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);
|
||||
$block->setChecksum($checksum);
|
||||
if ($key === false) {
|
||||
$block->disableCache();
|
||||
}
|
||||
|
||||
$grav->fireEvent('onFlexCollectionRender', new Event([
|
||||
'collection' => $this,
|
||||
'layout' => &$layout,
|
||||
'context' => &$context
|
||||
]));
|
||||
|
||||
$output = $this->getTemplate($layout)->render(
|
||||
['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'collection' => $this, 'layout' => $layout] + $context
|
||||
);
|
||||
|
||||
if ($debugger->enabled()) {
|
||||
$output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
|
||||
}
|
||||
|
||||
$block->setContent($output);
|
||||
|
||||
try {
|
||||
$cache && $block->isCached() && $cache->set($key, $block->toArray());
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-collection-' . $debugKey);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $type
|
||||
* @return $this
|
||||
*/
|
||||
public function setFlexDirectory(FlexDirectory $type)
|
||||
{
|
||||
$this->_flexDirectory = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(string $key) : array
|
||||
{
|
||||
$object = $this->get($key);
|
||||
|
||||
return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null)
|
||||
{
|
||||
return $this->_flexDirectory->getCache($namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string
|
||||
{
|
||||
return $this->_keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @return static
|
||||
*/
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
|
||||
{
|
||||
$list = $this->call('isAuthorized', [$action, $scope, $user]);
|
||||
$list = \array_filter($list);
|
||||
|
||||
return $this->select(array_keys($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @param string $field
|
||||
* @return FlexObject|null
|
||||
*/
|
||||
public function find($value, $field = 'id')
|
||||
{
|
||||
if ($value) foreach ($this as $element) {
|
||||
if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$elements = [];
|
||||
|
||||
/**
|
||||
* @var string $key
|
||||
* @var array|FlexObject $object
|
||||
*/
|
||||
foreach ($this->getElements() as $key => $object) {
|
||||
$elements[$key] = \is_array($object) ? $object : $object->jsonSerialize();
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'type:private' => $this->getFlexType(),
|
||||
'key:private' => $this->getKey(),
|
||||
'objects_key:private' => $this->getKeyField(),
|
||||
'objects:private' => $this->getElements()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from the specified elements.
|
||||
*
|
||||
* This method is provided for derived classes to specify how a new
|
||||
* instance should be created when constructor semantics have changed.
|
||||
*
|
||||
* @param array $elements Elements.
|
||||
* @param string|null $keyField
|
||||
*
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function createFrom(array $elements, $keyField = null)
|
||||
{
|
||||
$collection = new static($elements, $this->_flexDirectory);
|
||||
$collection->setKeyField($keyField ?: $this->_keyField);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix(): string
|
||||
{
|
||||
return 'c.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return 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()}/collection/{$layout}.html.twig",
|
||||
"flex-objects/layouts/_default/collection/{$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']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
protected function getRelatedDirectory($type): ?FlexDirectory
|
||||
{
|
||||
/** @var Flex $flex */
|
||||
$flex = Grav::instance()['flex_objects'];
|
||||
|
||||
return $flex->getDirectory($type);
|
||||
}
|
||||
|
||||
protected function setKeyField($keyField = null): void
|
||||
{
|
||||
$this->_keyField = $keyField ?? 'storage_key';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
<?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\Cache;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
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\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\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class FlexDirectory
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexDirectory implements FlexAuthorizeInterface
|
||||
{
|
||||
use FlexAuthorizeTrait;
|
||||
|
||||
/** @var string */
|
||||
protected $type;
|
||||
/** @var string */
|
||||
protected $blueprint_file;
|
||||
/** @var Blueprint[] */
|
||||
protected $blueprints;
|
||||
/** @var bool[] */
|
||||
protected $blueprints_init;
|
||||
/** @var FlexIndexInterface|null */
|
||||
protected $index;
|
||||
/** @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 string */
|
||||
protected $objectClassName;
|
||||
/** @var string */
|
||||
protected $collectionClassName;
|
||||
/** @var string */
|
||||
protected $indexClassName;
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
$this->config = new Config(array_merge_recursive($this->getBlueprintInternal()->get('config', []), $this->defaults));
|
||||
}
|
||||
|
||||
return null === $name ? $this->config : $this->config->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $context
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(string $type = '', string $context = '')
|
||||
{
|
||||
$blueprint = $this->getBlueprintInternal($type, $context);
|
||||
|
||||
if (empty($this->blueprints_init[$type])) {
|
||||
$this->blueprints_init[$type] = true;
|
||||
|
||||
$blueprint->setScope('object');
|
||||
$blueprint->init();
|
||||
if (empty($blueprint->fields())) {
|
||||
throw new RuntimeException(sprintf('Flex: Blueprint for %s is missing', $this->type));
|
||||
}
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 $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
|
||||
*/
|
||||
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
|
||||
{
|
||||
$index = clone $this->loadIndex();
|
||||
$index = $index->withKeyField($keyField);
|
||||
|
||||
if (null !== $keys) {
|
||||
$index = $index->select($keys);
|
||||
}
|
||||
|
||||
return $index->getIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object if it exists.
|
||||
*
|
||||
* Note: It is not safe to use the object without checking if the user can access it.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|null $keyField Field to be used as the key.
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject($key, string $keyField = null): ?FlexObjectInterface
|
||||
{
|
||||
return $this->getIndex(null, $keyField)->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string|null $key
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function update(array $data, string $key = null): FlexObjectInterface
|
||||
{
|
||||
$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) {
|
||||
$rows = $storage->replaceRows([$key => $object->prepareStorage()]);
|
||||
} else {
|
||||
$rows = $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
|
||||
*/
|
||||
public function remove(string $key): ?FlexObjectInterface
|
||||
{
|
||||
$object = $this->getIndex()->get($key);
|
||||
if (!$object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$object->delete();
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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('cache.' . $namespace);
|
||||
if (empty($config['enabled'])) {
|
||||
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
|
||||
} else {
|
||||
$timeout = $config['timeout'] ?? 60;
|
||||
|
||||
$key = $gravCache->getKey();
|
||||
if (Utils::isAdminPlugin()) {
|
||||
$key = substr($key, 0, -1);
|
||||
}
|
||||
$cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout);
|
||||
}
|
||||
} 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->index = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return string
|
||||
*/
|
||||
public function getStorageFolder(string $key = null): string
|
||||
{
|
||||
return $this->getStorage()->getStoragePath($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return string
|
||||
*/
|
||||
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 $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 $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', 'Grav\\Framework\\Flex\\FlexObject');
|
||||
}
|
||||
|
||||
return $this->objectClassName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCollectionClass(): string
|
||||
{
|
||||
if (!$this->collectionClassName) {
|
||||
$this->collectionClassName = $this->getConfig('data.collection', 'Grav\\Framework\\Flex\\FlexCollection');
|
||||
}
|
||||
|
||||
return $this->collectionClassName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIndexClass(): string
|
||||
{
|
||||
if (!$this->indexClassName) {
|
||||
$this->indexClassName = $this->getConfig('data.index', 'Grav\\Framework\\Flex\\FlexIndex');
|
||||
}
|
||||
|
||||
return $this->indexClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string $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'];
|
||||
$debugger->startTimer('flex-objects', sprintf('Flex: Initializing %d %s', \count($entries), $this->type));
|
||||
|
||||
$storage = $this->getStorage();
|
||||
$cache = $this->getCache('object');
|
||||
|
||||
// Get storage keys for the objects.
|
||||
$keys = [];
|
||||
$rows = [];
|
||||
foreach ($entries as $key => $value) {
|
||||
$k = $value['storage_key'];
|
||||
$keys[$k] = $key;
|
||||
$rows[$k] = null;
|
||||
}
|
||||
|
||||
// Fetch rows from the cache.
|
||||
try {
|
||||
$rows = $cache->getMultiple(array_keys($rows));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
|
||||
// Read missing rows from the storage.
|
||||
$updated = [];
|
||||
$rows = $storage->readRows($rows, $updated);
|
||||
|
||||
// Store updated rows to the cache.
|
||||
if ($updated) {
|
||||
try {
|
||||
if (!$cache instanceof MemoryCache) {
|
||||
$debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug');
|
||||
}
|
||||
$cache->setMultiple($updated);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
|
||||
// TODO: log about the issue.
|
||||
}
|
||||
}
|
||||
|
||||
// Create objects from the rows.
|
||||
$list = [];
|
||||
foreach ($rows as $storageKey => $row) {
|
||||
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;
|
||||
}
|
||||
|
||||
$usedKey = $keys[$storageKey];
|
||||
$row += [
|
||||
'storage_key' => $storageKey,
|
||||
'storage_timestamp' => $entries[$usedKey]['storage_timestamp'],
|
||||
];
|
||||
|
||||
$key = $entries[$usedKey]['key'] ?? $usedKey;
|
||||
$object = $this->createObject($row, $key, false);
|
||||
$list[$usedKey] = $object;
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-objects');
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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));
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexIndexInterface
|
||||
*/
|
||||
protected function loadIndex(): FlexIndexInterface
|
||||
{
|
||||
static $i = 0;
|
||||
|
||||
$index = $this->index;
|
||||
|
||||
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 (null === $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.
|
||||
}
|
||||
}
|
||||
|
||||
// We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop.
|
||||
$this->index = $this->createIndex($keys);
|
||||
/** @var FlexCollectionInterface $collection */
|
||||
$collection = $this->index->orderBy($this->getConfig('data.ordering', []));
|
||||
$this->index = $index = $collection->getIndex();
|
||||
|
||||
$debugger->stopTimer('flex-keys-' . $this->type . $j);
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
<?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\Data\Data;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Form\Traits\FormTrait;
|
||||
use Grav\Framework\Route\Route;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/**
|
||||
* Class FlexForm
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexForm implements FlexFormInterface
|
||||
{
|
||||
use FormTrait {
|
||||
FormTrait::doSerialize as doTraitSerialize;
|
||||
FormTrait::doUnserialize as doTraitUnserialize;
|
||||
}
|
||||
|
||||
/** @var array|null */
|
||||
private $form;
|
||||
|
||||
/** @var FlexObjectInterface */
|
||||
private $object;
|
||||
|
||||
/**
|
||||
* FlexForm constructor.
|
||||
* @param string $name
|
||||
* @param FlexObjectInterface $object
|
||||
* @param array|null $form
|
||||
*/
|
||||
public function __construct(string $name, FlexObjectInterface $object, array $form = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->form = $form;
|
||||
|
||||
$uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getFlexType()}:new";
|
||||
$this->setObject($object);
|
||||
$this->setId($this->getName());
|
||||
$this->setUniqueId(md5($uniqueId));
|
||||
$this->messages = [];
|
||||
$this->submitted = false;
|
||||
|
||||
$flash = $this->getFlash();
|
||||
if ($flash->exists()) {
|
||||
$data = $flash->getData();
|
||||
$includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
|
||||
|
||||
$this->data = $data ? new Data($data, $this->getBlueprint()) : null;
|
||||
$this->files = $flash->getFilesByFields($includeOriginal);
|
||||
} else {
|
||||
$this->data = null;
|
||||
$this->files = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
$object = $this->getObject();
|
||||
$name = $this->name ?: 'object';
|
||||
|
||||
return "flex-{$object->getFlexType()}-{$name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data|FlexObjectInterface|object
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data ?? $this->getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the form.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(string $name)
|
||||
{
|
||||
// Attempt to get value from the form data.
|
||||
$value = $this->data ? $this->data[$name] : null;
|
||||
|
||||
// Return the form data or fall back to the object property.
|
||||
return $value ?? $this->getObject()->getFormValue($name);
|
||||
}
|
||||
|
||||
public function getDefaultValue(string $name)
|
||||
{
|
||||
return $this->object->getDefaultValue($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultValues(): array
|
||||
{
|
||||
return $this->object->getDefaultValues();
|
||||
}
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->object->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function getObject(): FlexObjectInterface
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
public function updateObject(): FlexObjectInterface
|
||||
{
|
||||
$data = $this->data instanceof Data ? $this->data->toArray() : [];
|
||||
$files = $this->files;
|
||||
|
||||
return $this->getObject()->update($data, $files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(): Blueprint
|
||||
{
|
||||
if (null === $this->blueprint) {
|
||||
try {
|
||||
$blueprint = $this->getObject()->getBlueprint(Utils::isAdminPlugin() ? '' : $this->name);
|
||||
if ($this->form) {
|
||||
// We have field overrides available.
|
||||
$blueprint->extend(['form' => $this->form], true);
|
||||
$blueprint->init();
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
if (!isset($this->form['fields'])) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Blueprint is not defined, but we have custom form fields available.
|
||||
$blueprint = new Blueprint(null, ['form' => $this->form]);
|
||||
$blueprint->load();
|
||||
$blueprint->setScope('object');
|
||||
$blueprint->init();
|
||||
}
|
||||
|
||||
$this->blueprint = $blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileUploadAjaxRoute(): ?Route
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (!method_exists($object, 'route')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $object->route('/edit.json/task:media.upload');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $filename
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileDeleteAjaxRoute($field, $filename): ?Route
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (!method_exists($object, 'route')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $object->route('/edit.json/task:media.delete');
|
||||
}
|
||||
|
||||
public function getMediaTaskRoute(array $params = [], $extension = null): string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
/** @var Flex $flex */
|
||||
$flex = $grav['flex_objects'];
|
||||
|
||||
if (method_exists($flex, 'adminRoute')) {
|
||||
return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Serializable::unserialize().
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function unserialize($data): void
|
||||
{
|
||||
$data = unserialize($data, ['allowed_classes' => [FlexObject::class]]);
|
||||
|
||||
$this->doUnserialize($data);
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return $form[$name] ?? null;
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$method = "set{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}($value);
|
||||
}
|
||||
}
|
||||
|
||||
public function __isset($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return isset($form[$name]);
|
||||
}
|
||||
|
||||
public function __unset($name)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method clones the object.
|
||||
*
|
||||
* @param FlexObjectInterface $object
|
||||
* @return $this
|
||||
*/
|
||||
protected function setObject(FlexObjectInterface $object): self
|
||||
{
|
||||
$this->object = clone $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
return $twig->twig()->resolveTemplate(
|
||||
[
|
||||
"flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
|
||||
"flex-objects/layouts/_default/form/{$layout}.html.twig",
|
||||
"forms/{$layout}/form.html.twig",
|
||||
'forms/default/form.html.twig'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function doSubmit(array $data, array $files)
|
||||
{
|
||||
/** @var FlexObject $object */
|
||||
$object = clone $this->getObject();
|
||||
|
||||
$object->update($data, $files);
|
||||
$object->save();
|
||||
|
||||
$this->setObject($object);
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
return $this->doTraitSerialize() + [
|
||||
'object' => $this->object,
|
||||
];
|
||||
}
|
||||
|
||||
protected function doUnserialize(array $data): void
|
||||
{
|
||||
$this->doTraitUnserialize($data);
|
||||
|
||||
$this->object = $data['object'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter validated data.
|
||||
*
|
||||
* @param \ArrayAccess $data
|
||||
*/
|
||||
protected function filterData(\ArrayAccess $data): void
|
||||
{
|
||||
if ($data instanceof Data) {
|
||||
$data->filter(true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
<?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\Debugger;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Session;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\Collection\CollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectInterface;
|
||||
use Grav\Framework\Object\ObjectIndex;
|
||||
use Monolog\Logger;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
|
||||
class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface
|
||||
{
|
||||
/** @var FlexDirectory */
|
||||
private $_flexDirectory;
|
||||
|
||||
/** @var string */
|
||||
private $_keyField;
|
||||
|
||||
/** @var array */
|
||||
private $_indexKeys;
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
* @return static
|
||||
*/
|
||||
public static function createFromStorage(FlexDirectory $directory)
|
||||
{
|
||||
return static::createFromArray(static::loadEntriesFromStorage($directory->getStorage()), $directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::createFromArray()
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
|
||||
{
|
||||
$instance = new static($entries, $directory);
|
||||
$instance->setKeyField($keyField);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return array
|
||||
*/
|
||||
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
|
||||
{
|
||||
return $storage->getExistingKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new FlexIndex.
|
||||
*
|
||||
* @param array $entries
|
||||
* @param FlexDirectory|null $directory
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null)
|
||||
{
|
||||
parent::__construct($entries);
|
||||
|
||||
$this->_flexDirectory = $directory;
|
||||
$this->setKeyField(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::search()
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null)
|
||||
{
|
||||
return $this->__call('search', [$search, $properties, $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::sort()
|
||||
*/
|
||||
public function sort(array $orderings)
|
||||
{
|
||||
return $this->orderBy($orderings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::filterBy()
|
||||
*/
|
||||
public function filterBy(array $filters)
|
||||
{
|
||||
return $this->__call('filterBy', [$filters]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexType()
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->_flexDirectory->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory
|
||||
{
|
||||
return $this->_flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamp()
|
||||
*/
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
$timestamps = $this->getTimestamps();
|
||||
|
||||
return $timestamps ? max($timestamps) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getCacheKey()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getCacheChecksum()
|
||||
*/
|
||||
public function getCacheChecksum(): string
|
||||
{
|
||||
return sha1($this->getCacheKey() . json_encode($this->getTimestamps()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamps()
|
||||
*/
|
||||
public function getTimestamps(): array
|
||||
{
|
||||
return $this->getIndexMap('storage_timestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getStorageKeys()
|
||||
*/
|
||||
public function getStorageKeys(): array
|
||||
{
|
||||
return $this->getIndexMap('storage_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexKeys()
|
||||
*/
|
||||
public function getFlexKeys(): array
|
||||
{
|
||||
// Get storage keys for the objects.
|
||||
$keys = [];
|
||||
$type = $this->_flexDirectory->getFlexType() . '.obj:';
|
||||
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$keys[$key] = $value['flex_key'] ?? $type . $value['storage_key'];
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexIndexInterface::withKeyField()
|
||||
*/
|
||||
public function withKeyField(string $keyField = null)
|
||||
{
|
||||
$keyField = $keyField ?: 'key';
|
||||
if ($keyField === $this->getKeyField()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$type = $keyField === 'flex_key' ? $this->_flexDirectory->getFlexType() . '.obj:' : '';
|
||||
$entries = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
if (!isset($value['key'])) {
|
||||
$value['key'] = $key;
|
||||
}
|
||||
|
||||
if (isset($value[$keyField])) {
|
||||
$entries[$value[$keyField]] = $value;
|
||||
} elseif ($keyField === 'flex_key') {
|
||||
$entries[$type . $value['storage_key']] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries, $keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getIndex()
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::render()
|
||||
*/
|
||||
public function render(string $layout = null, array $context = [])
|
||||
{
|
||||
return $this->__call('render', [$layout, $context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexIndexInterface::getFlexKeys()
|
||||
*/
|
||||
public function getIndexMap(string $indexKey = null)
|
||||
{
|
||||
if (null === $indexKey) {
|
||||
return $this->getEntries();
|
||||
}
|
||||
|
||||
// Get storage keys for the objects.
|
||||
$index = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$index[$key] = $value[$indexKey] ?? null;
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(string $key): array
|
||||
{
|
||||
return $this->getEntries()[$key] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField() : string
|
||||
{
|
||||
return $this->_keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null)
|
||||
{
|
||||
return $this->_flexDirectory->getCache($namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $orderings
|
||||
* @return FlexIndex|FlexCollection
|
||||
*/
|
||||
public function orderBy(array $orderings)
|
||||
{
|
||||
if (!$orderings || !$this->count()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Check if ordering needs to load the objects.
|
||||
if (array_diff_key($orderings, $this->getIndexKeys())) {
|
||||
return $this->__call('orderBy', [$orderings]);
|
||||
}
|
||||
|
||||
// Ordering can be done by using index only.
|
||||
$previous = null;
|
||||
foreach (array_reverse($orderings) as $field => $ordering) {
|
||||
$field = (string)$field;
|
||||
if ($this->getKeyField() === $field) {
|
||||
$keys = $this->getKeys();
|
||||
$search = array_combine($keys, $keys) ?: [];
|
||||
} elseif ($field === 'flex_key') {
|
||||
$search = $this->getFlexKeys();
|
||||
} else {
|
||||
$search = $this->getIndexMap($field);
|
||||
}
|
||||
|
||||
// Update current search to match the previous ordering.
|
||||
if (null !== $previous) {
|
||||
$search = array_replace($previous, $search);
|
||||
}
|
||||
|
||||
// Order by current field.
|
||||
if ($ordering === 'DESC') {
|
||||
arsort($search, SORT_NATURAL);
|
||||
} else {
|
||||
asort($search, SORT_NATURAL);
|
||||
}
|
||||
|
||||
$previous = $search;
|
||||
}
|
||||
|
||||
return $this->createFrom(array_replace($previous, $this->getEntries()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function call($method, array $arguments = [])
|
||||
{
|
||||
return $this->__call('call', [$method, $arguments]);
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
|
||||
/** @var FlexCollection $className */
|
||||
$className = $this->_flexDirectory->getCollectionClass();
|
||||
$cachedMethods = $className::getCachedMethods();
|
||||
|
||||
$flexType = $this->getFlexType();
|
||||
|
||||
if (!empty($cachedMethods[$name])) {
|
||||
$type = $cachedMethods[$name];
|
||||
if ($type === 'session') {
|
||||
/** @var Session $session */
|
||||
$session = Grav::instance()['session'];
|
||||
$cacheKey = $session->getId() . $session->user->username;
|
||||
} else {
|
||||
$cacheKey = '';
|
||||
}
|
||||
$key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey());
|
||||
|
||||
$cache = $this->getCache('object');
|
||||
|
||||
try {
|
||||
$result = $cache->get($key);
|
||||
|
||||
// Make sure the keys aren't changed if the returned type is the same index type.
|
||||
if ($result instanceof self && $flexType === $result->getFlexType()) {
|
||||
$result = $result->withKeyField($this->getKeyField());
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
}
|
||||
|
||||
if (!isset($result)) {
|
||||
$collection = $this->loadCollection();
|
||||
$result = $collection->{$name}(...$arguments);
|
||||
|
||||
try {
|
||||
// If flex collection is returned, convert it back to flex index.
|
||||
if ($result instanceof FlexCollection) {
|
||||
$cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField());
|
||||
} else {
|
||||
$cached = $result;
|
||||
}
|
||||
|
||||
$cache->set($key, $cached);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
|
||||
// TODO: log error.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$collection = $this->loadCollection();
|
||||
$result = $collection->{$name}(...$arguments);
|
||||
if (!isset($cachedMethods[$name])) {
|
||||
$class = \get_class($collection);
|
||||
$debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize(['type' => $this->getFlexType(), 'entries' => $this->getEntries()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized, ['allowed_classes' => false]);
|
||||
|
||||
$this->_flexDirectory = Grav::instance()['flex_objects']->getDirectory($data['type']);
|
||||
$this->setEntries($data['entries']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string $keyField
|
||||
* @return static
|
||||
*/
|
||||
protected function createFrom(array $entries, string $keyField = null)
|
||||
{
|
||||
$index = new static($entries, $this->_flexDirectory);
|
||||
$index->setKeyField($keyField ?? $this->_keyField);
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $keyField
|
||||
*/
|
||||
protected function setKeyField(string $keyField = null)
|
||||
{
|
||||
$this->_keyField = $keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
protected function getIndexKeys()
|
||||
{
|
||||
if (null === $this->_indexKeys) {
|
||||
$entries = $this->getEntries();
|
||||
$first = reset($entries);
|
||||
if ($first) {
|
||||
$keys = array_keys($first);
|
||||
$keys = array_combine($keys, $keys) ?: [];
|
||||
} else {
|
||||
$keys = [];
|
||||
}
|
||||
|
||||
$this->setIndexKeys($keys);
|
||||
}
|
||||
|
||||
return $this->_indexKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $indexKeys
|
||||
*/
|
||||
protected function setIndexKeys(array $indexKeys)
|
||||
{
|
||||
// Add defaults.
|
||||
$indexKeys += [
|
||||
'key' => 'key',
|
||||
'storage_key' => 'storage_key',
|
||||
'storage_timestamp' => 'storage_timestamp',
|
||||
'flex_key' => 'flex_key'
|
||||
];
|
||||
|
||||
|
||||
$this->_indexKeys = $indexKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix()
|
||||
{
|
||||
return 'i.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return ObjectInterface|null
|
||||
*/
|
||||
protected function loadElement($key, $value): ?ObjectInterface
|
||||
{
|
||||
$objects = $this->_flexDirectory->loadObjects([$key => $value]);
|
||||
|
||||
return $objects ? reset($objects): null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return ObjectInterface[]
|
||||
*/
|
||||
protected function loadElements(array $entries = null): array
|
||||
{
|
||||
return $this->_flexDirectory->loadObjects($entries ?? $this->getEntries());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return ObjectCollectionInterface
|
||||
*/
|
||||
protected function loadCollection(array $entries = null): CollectionInterface
|
||||
{
|
||||
return $this->_flexDirectory->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAllowedElement($value): bool
|
||||
{
|
||||
return $value instanceof FlexObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexObjectInterface $object
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getElementMeta($object)
|
||||
{
|
||||
return $object->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @param array $index Saved index
|
||||
* @param array $entries Updated index
|
||||
* @return array Compiled list of entries
|
||||
*/
|
||||
protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries): array
|
||||
{
|
||||
// Calculate removed objects.
|
||||
$removed = array_diff_key($index, $entries);
|
||||
|
||||
// First get rid of all removed objects.
|
||||
if ($removed) {
|
||||
$index = array_diff_key($index, $removed);
|
||||
}
|
||||
|
||||
if ($entries) {
|
||||
// Calculate difference between saved index and current data.
|
||||
foreach ($index as $key => $entry) {
|
||||
$storage_key = $entry['storage_key'] ?? null;
|
||||
if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) {
|
||||
// Entry is up to date, no update needed.
|
||||
unset($entries[$storage_key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($entries) && empty($removed)) {
|
||||
// No objects were added, updated or removed.
|
||||
return $index;
|
||||
}
|
||||
} elseif (!$removed) {
|
||||
// There are no objects and nothing was removed.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Index should be updated, lock the index file for saving.
|
||||
$indexFile = static::getIndexFile($storage);
|
||||
$indexFile->lock();
|
||||
|
||||
// Read all the data rows into an array.
|
||||
$keys = array_fill_keys(array_keys($entries), null);
|
||||
$rows = $storage->readRows($keys);
|
||||
|
||||
$keyField = $storage->getKeyField();
|
||||
|
||||
// Go through all the updated objects and refresh their index data.
|
||||
$updated = $added = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null !== $row) {
|
||||
$entry = ['key' => $key] + $entries[$key];
|
||||
if ($keyField !== 'storage_key' && isset($row[$keyField])) {
|
||||
$entry['key'] = $row[$keyField];
|
||||
}
|
||||
static::updateIndexData($entry, $row);
|
||||
if (isset($row['__error'])) {
|
||||
$entry['__error'] = true;
|
||||
static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error'])));
|
||||
}
|
||||
if (isset($index[$key])) {
|
||||
// Update object in the index.
|
||||
$updated[$key] = $entry;
|
||||
} else {
|
||||
// Add object into the index.
|
||||
$added[$key] = $entry;
|
||||
}
|
||||
|
||||
// Either way, update the entry.
|
||||
$index[$key] = $entry;
|
||||
} elseif (isset($index[$key])) {
|
||||
// Remove object from the index.
|
||||
$removed[$key] = $index[$key];
|
||||
unset($index[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the index before saving it.
|
||||
ksort($index, SORT_NATURAL);
|
||||
|
||||
static::onChanges($index, $added, $updated, $removed);
|
||||
|
||||
$indexFile->save(['count' => \count($index), 'index' => $index]);
|
||||
$indexFile->unlock();
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
protected static function updateIndexData(array &$entry, array $data)
|
||||
{
|
||||
}
|
||||
|
||||
protected static function loadEntriesFromIndex(FlexStorageInterface $storage)
|
||||
{
|
||||
$indexFile = static::getIndexFile($storage);
|
||||
|
||||
$data = [];
|
||||
try {
|
||||
$data = (array)$indexFile->content();
|
||||
} catch (\Exception $e) {
|
||||
$e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e);
|
||||
|
||||
static::onException($e);
|
||||
}
|
||||
|
||||
return $data['index'] ?? [];
|
||||
}
|
||||
|
||||
protected static function getIndexFile(FlexStorageInterface $storage)
|
||||
{
|
||||
// Load saved index file.
|
||||
$grav = Grav::instance();
|
||||
$locator = $grav['locator'];
|
||||
$filename = $locator->findResource($storage->getStoragePath() . '/index.yaml', true, true);
|
||||
|
||||
return CompiledYamlFile::instance($filename);
|
||||
}
|
||||
|
||||
protected static function onException(\Exception $e)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Logger $logger */
|
||||
$logger = $grav['log'];
|
||||
$logger->addAlert($e->getMessage());
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addException($e);
|
||||
$debugger->addMessage($e, 'error');
|
||||
}
|
||||
|
||||
protected static function onChanges(array $entries, array $added, array $updated, array $removed)
|
||||
{
|
||||
$message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed));
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Logger $logger */
|
||||
$logger = $grav['log'];
|
||||
$logger->addDebug($message);
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addMessage($message, 'debug');
|
||||
}
|
||||
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'type:private' => $this->getFlexType(),
|
||||
'key:private' => $this->getKey(),
|
||||
'entries_key:private' => $this->getKeyField(),
|
||||
'entries:private' => $this->getEntries()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
|
||||
/**
|
||||
* Defines authorization checks for Flex Objects.
|
||||
*/
|
||||
interface FlexAuthorizeInterface
|
||||
{
|
||||
/**
|
||||
* Check if user is authorized to perform an action for the object.
|
||||
*
|
||||
* @param string $action One of: `create`, `read`, `update`, `delete`, `save`, `list`
|
||||
* @param string|null $scope One of: `admin`, `site`
|
||||
* @param UserInterface|null $user Optional user. Defaults to the current user.
|
||||
*
|
||||
* @return bool Returns `true` if user is authorized to perform action, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\Flex;
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
|
||||
/**
|
||||
* Defines a collection of Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexCollection
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexCollectionInterface extends FlexCommonInterface, ObjectCollectionInterface, NestedObjectInterface
|
||||
{
|
||||
/**
|
||||
* Creates a Flex Collection from an array.
|
||||
*
|
||||
* @used-by FlexDirectory::createCollection() Official method to create a Flex Collection.
|
||||
*
|
||||
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
|
||||
* @param FlexDirectory $directory Flex Directory where all the objects belong into.
|
||||
* @param string $keyField Key field used to index the collection.
|
||||
*
|
||||
* @return static Returns a new Flex Collection.
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null);
|
||||
|
||||
/**
|
||||
* Creates a new Flex Collection.
|
||||
*
|
||||
* @used-by FlexDirectory::createCollection() Official method to create Flex Collection.
|
||||
*
|
||||
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
|
||||
* @param FlexDirectory $directory Flex Directory where all the objects belong into.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null);
|
||||
|
||||
/**
|
||||
* Search a string from the collection.
|
||||
*
|
||||
* @param string $search Search string.
|
||||
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
|
||||
* @param array|null $options Search options, defaults to configured options.
|
||||
*
|
||||
* @return FlexCollectionInterface Returns a Flex Collection with only matching objects.
|
||||
* @api
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null);
|
||||
|
||||
/**
|
||||
* Sort the collection.
|
||||
*
|
||||
* @param array $orderings Pair of [property => 'ASC'|'DESC', ...].
|
||||
*
|
||||
* @return FlexCollectionInterface Returns a sorted version from the collection.
|
||||
*/
|
||||
public function sort(array $orderings);
|
||||
|
||||
/**
|
||||
* Filter collection by filter array with keys and values.
|
||||
*
|
||||
* @param array $filters
|
||||
* @return FlexCollectionInterface
|
||||
*/
|
||||
public function filterBy(array $filters);
|
||||
|
||||
/**
|
||||
* Get timestamps from all the objects in the collection.
|
||||
*
|
||||
* This method can be used for example in caching.
|
||||
*
|
||||
* @return int[] Returns [key => timestamp, ...] pairs.
|
||||
*/
|
||||
public function getTimestamps(): array;
|
||||
|
||||
/**
|
||||
* Get storage keys from all the objects in the collection.
|
||||
*
|
||||
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
|
||||
*
|
||||
* @return string[] Returns [key => storage_key, ...] pairs.
|
||||
*/
|
||||
public function getStorageKeys(): array;
|
||||
|
||||
/**
|
||||
* Get Flex keys from all the objects in the collection.
|
||||
*
|
||||
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
|
||||
*
|
||||
* @return string[] Returns[key => flex_key, ...] pairs.
|
||||
*/
|
||||
public function getFlexKeys(): array;
|
||||
|
||||
/**
|
||||
* Return new collection with a different key.
|
||||
*
|
||||
* @param string|null $keyField Switch key field of the collection.
|
||||
*
|
||||
* @return FlexCollectionInterface Returns a new Flex Collection with new key field.
|
||||
* @api
|
||||
*/
|
||||
public function withKeyField(string $keyField = null);
|
||||
|
||||
/**
|
||||
* Get Flex Index from the Flex Collection.
|
||||
*
|
||||
* @return FlexIndexInterface Returns a Flex Index from the current collection.
|
||||
*/
|
||||
public function getIndex();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Interfaces\RenderInterface;
|
||||
|
||||
/**
|
||||
* Defines common interface shared with both Flex Objects and Collections.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexObject
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexCommonInterface extends RenderInterface
|
||||
{
|
||||
/**
|
||||
* Get Flex Type of the object / collection.
|
||||
*
|
||||
* @return string Returns Flex Type of the collection.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexType(): string;
|
||||
|
||||
/**
|
||||
* Get Flex Directory for the object / collection.
|
||||
*
|
||||
* @return FlexDirectory Returns associated Flex Directory.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory;
|
||||
|
||||
/**
|
||||
* Get last updated timestamp for the object / collection.
|
||||
*
|
||||
* @return int Returns Unix timestamp.
|
||||
* @api
|
||||
*/
|
||||
public function getTimestamp(): int;
|
||||
|
||||
/**
|
||||
* Get a cache key which is used for caching the object / collection.
|
||||
*
|
||||
* @return string Returns cache key.
|
||||
*/
|
||||
public function getCacheKey(): string;
|
||||
|
||||
/**
|
||||
* Get cache checksum for the object / collection.
|
||||
*
|
||||
* If checksum changes, cache gets invalided.
|
||||
*
|
||||
* @return string Returns cache checksum.
|
||||
*/
|
||||
public function getCacheChecksum(): string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Interfaces;
|
||||
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
use Grav\Framework\Route\Route;
|
||||
|
||||
/**
|
||||
* Defines Forms for Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexForm
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexFormInterface extends \Serializable, FormInterface
|
||||
{
|
||||
/**
|
||||
* Get object associated to the form.
|
||||
*
|
||||
* @return FlexObjectInterface Returns Flex Object associated to the form.
|
||||
* @api
|
||||
*/
|
||||
public function getObject();
|
||||
|
||||
/**
|
||||
* Get media task route.
|
||||
*
|
||||
* @return string Returns admin route for media tasks.
|
||||
*/
|
||||
public function getMediaTaskRoute(): string;
|
||||
|
||||
/**
|
||||
* Get route for uploading files by AJAX.
|
||||
*
|
||||
* @return Route|null Returns Route object or null if file uploads are not enabled.
|
||||
*/
|
||||
public function getFileUploadAjaxRoute();
|
||||
|
||||
/**
|
||||
* Get route for deleting files by AJAX.
|
||||
*
|
||||
* @param string $field Field where the file is associated into.
|
||||
* @param string $filename Filename for the file.
|
||||
*
|
||||
* @return Route|null Returns Route object or null if file uploads are not enabled.
|
||||
*/
|
||||
public function getFileDeleteAjaxRoute($field, $filename);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
|
||||
/**
|
||||
* Defines Indexes for Flex Objects.
|
||||
*
|
||||
* Flex indexes are similar to database indexes, they contain indexed fields which can be used to quickly look up or
|
||||
* find the objects without loading them.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexIndex
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexIndexInterface extends FlexCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Helper method to create Flex Index.
|
||||
*
|
||||
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
|
||||
*
|
||||
* @param FlexDirectory $directory Flex directory.
|
||||
*
|
||||
* @return static Returns a new Flex Index.
|
||||
*/
|
||||
public static function createFromStorage(FlexDirectory $directory);
|
||||
|
||||
/**
|
||||
* Method to load index from the object storage, usually filesystem.
|
||||
*
|
||||
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
|
||||
*
|
||||
* @param FlexStorageInterface $storage Flex Storage associated to the directory.
|
||||
*
|
||||
* @return array Returns a list of existing objects [storage_key => [storage_key => xxx, storage_timestamp => 123456, ...]]
|
||||
*/
|
||||
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array;
|
||||
|
||||
/**
|
||||
* Return new collection with a different key.
|
||||
*
|
||||
* @param string|null $keyField Switch key field of the collection.
|
||||
*
|
||||
* @return FlexIndexInterface Returns a new Flex Collection with new key field.
|
||||
* @api
|
||||
*/
|
||||
public function withKeyField(string $keyField = null);
|
||||
|
||||
/**
|
||||
* @param string $indexKey
|
||||
* @return array
|
||||
*/
|
||||
public function getIndexMap(string $indexKey = null);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Framework\Flex\Flex;
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Defines Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexObject
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface, \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* Construct a new Flex Object instance.
|
||||
*
|
||||
* @used-by FlexDirectory::createObject() Method to create Flex Object.
|
||||
*
|
||||
* @param array $elements Array of object properties.
|
||||
* @param string $key Identifier key for the new object.
|
||||
* @param FlexDirectory $directory Flex Directory the object belongs into.
|
||||
* @param bool $validate True if the object should be validated against blueprint.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false);
|
||||
|
||||
/**
|
||||
* Search a string from the object, returns weight between 0 and 1.
|
||||
*
|
||||
* Note: If you override this function, make sure you return value in range 0...1!
|
||||
*
|
||||
* @used-by FlexCollectionInterface::search() If you want to search a string from a Flex Collection.
|
||||
*
|
||||
* @param string $search Search string.
|
||||
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
|
||||
* @param array|null $options Search options, defaults to configured options.
|
||||
*
|
||||
* @return float Returns a weight between 0 and 1.
|
||||
* @api
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null): float;
|
||||
|
||||
/**
|
||||
* Get a unique key for the object.
|
||||
*
|
||||
* Flex Keys can be used without knowing the Directory the Object belongs into.
|
||||
*
|
||||
* @see Flex::getObject() If you want to get Flex Object from any Flex Directory.
|
||||
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
|
||||
*
|
||||
* NOTE: Please do not override the method!
|
||||
*
|
||||
* @return string Returns Flex Key of the object.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexKey(): string;
|
||||
|
||||
/**
|
||||
* Get an unique storage key (within the directory) which is used for figuring out the filename or database id.
|
||||
*
|
||||
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
|
||||
* @see FlexDirectory::getCollection() If you want to get Flex Collection with selected keys from the Flex Directory.
|
||||
*
|
||||
* @return string Returns storage key of the Object.
|
||||
* @api
|
||||
*/
|
||||
public function getStorageKey(): string;
|
||||
|
||||
/**
|
||||
* Get index data associated to the object.
|
||||
*
|
||||
* @return array Returns metadata of the object.
|
||||
*/
|
||||
public function getMetaData(): array;
|
||||
|
||||
/**
|
||||
* Returns true if the object exists in the storage.
|
||||
*
|
||||
* @return bool Returns `true` if the object exists, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function exists(): bool;
|
||||
|
||||
/**
|
||||
* Prepare object for saving into the storage.
|
||||
*
|
||||
* @return array Returns an array of object properties containing only scalars and arrays.
|
||||
*/
|
||||
public function prepareStorage(): array;
|
||||
|
||||
/**
|
||||
* Updates object in the memory.
|
||||
*
|
||||
* @see FlexObjectInterface::save() You need to save the object after calling this method.
|
||||
*
|
||||
* @param array $data Data containing updated properties with their values. To unset a value, use `null`.
|
||||
* @param array|UploadedFileInterface[] $files List of uploaded files to be saved within the object.
|
||||
*
|
||||
* @return FlexObjectInterface
|
||||
* @throws \RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function update(array $data, array $files = []);
|
||||
|
||||
/**
|
||||
* Create new object into the storage.
|
||||
*
|
||||
* @see FlexDirectory::createObject() If you want to create a new object instance.
|
||||
* @see FlexObjectInterface::update() If you want to update properties of the object.
|
||||
*
|
||||
* @param string|null $key Optional new key. If key isn't given, random key will be associated to the object.
|
||||
*
|
||||
* @return FlexObjectInterface
|
||||
* @throws \RuntimeException if object already exists.
|
||||
* @api
|
||||
*/
|
||||
public function create(string $key = null);
|
||||
|
||||
/**
|
||||
* Save object into the storage.
|
||||
*
|
||||
* @see FlexObjectInterface::update() If you want to update properties of the object.
|
||||
*
|
||||
* @return FlexObjectInterface
|
||||
* @api
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Delete object from the storage.
|
||||
*
|
||||
* @return FlexObjectInterface
|
||||
* @api
|
||||
*/
|
||||
public function delete();
|
||||
|
||||
/**
|
||||
* Returns the blueprint of the object.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
* @used-by FlexForm::getBlueprint()
|
||||
*
|
||||
* @param string $name Name of the Blueprint form. Used to create customized forms for different use cases.
|
||||
*
|
||||
* @return Blueprint Returns a Blueprint.
|
||||
*/
|
||||
public function getBlueprint(string $name = '');
|
||||
|
||||
/**
|
||||
* Returns a form instance for the object.
|
||||
*
|
||||
* @param string $name Name of the form. Can be used to create customized forms for different use cases.
|
||||
* @param array|null $form Can be used to further customize the form.
|
||||
*
|
||||
* @return FlexFormInterface Returns a Form.
|
||||
* @api
|
||||
*/
|
||||
public function getForm(string $name = '', array $form = null);
|
||||
|
||||
/**
|
||||
* Returns default value suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @param string $name Property name.
|
||||
* @param string $separator Optional nested property separator.
|
||||
*
|
||||
* @return mixed|null Returns default value of the field, null if there is no default value.
|
||||
*/
|
||||
public function getDefaultValue(string $name, string $separator = null);
|
||||
|
||||
/**
|
||||
* Returns default values suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @return array Returns default values.
|
||||
*/
|
||||
public function getDefaultValues(): array;
|
||||
|
||||
/**
|
||||
* Returns raw value suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @param string $name Property name.
|
||||
* @param mixed $default Default value.
|
||||
* @param string $separator Optional nested property separator.
|
||||
*
|
||||
* @return mixed Returns value of the field.
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Interfaces;
|
||||
|
||||
/**
|
||||
* Defines Flex Storage layer.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexStorageInterface
|
||||
{
|
||||
/**
|
||||
* StorageInterface constructor.
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string;
|
||||
|
||||
/**
|
||||
* Returns associated array of all existing storage keys with a timestamp.
|
||||
*
|
||||
* @return array Returns all existing keys as `[key => [storage_key => key, storage_timestamp => timestamp], ...]`.
|
||||
*/
|
||||
public function getExistingKeys(): array;
|
||||
|
||||
/**
|
||||
* Check if the key exists in the storage.
|
||||
*
|
||||
* @param string $key Storage key of an object.
|
||||
*
|
||||
* @return bool Returns `true` if the key exists in the storage, `false` otherwise.
|
||||
*/
|
||||
public function hasKey(string $key): bool;
|
||||
|
||||
/**
|
||||
* Check if the key exists in the storage.
|
||||
*
|
||||
* @param string[] $keys Storage key of an object.
|
||||
*
|
||||
* @return bool[] Returns keys with `true` if the key exists in the storage, `false` otherwise.
|
||||
*/
|
||||
public function hasKeys(array $keys): array;
|
||||
|
||||
/**
|
||||
* Create new rows into the storage.
|
||||
*
|
||||
* New keys will be assigned when the objects are created.
|
||||
*
|
||||
* @param array $rows List of rows as `[row, ...]`.
|
||||
*
|
||||
* @return array Returns created rows as `[key => row, ...] pairs.
|
||||
*/
|
||||
public function createRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Read rows from the storage.
|
||||
*
|
||||
* If you pass object or array as value, that value will be used to save I/O.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
* @param array $fetched Optional reference to store only fetched items.
|
||||
*
|
||||
* @return array Returns rows. Note that non-existing rows will have `null` as their value.
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array;
|
||||
|
||||
/**
|
||||
* Update existing rows in the storage.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
*
|
||||
* @return array Returns updated rows. Note that non-existing rows will not be saved and have `null` as their value.
|
||||
*/
|
||||
public function updateRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Delete rows from the storage.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
*
|
||||
* @return array Returns deleted rows. Note that non-existing rows have `null` as their value.
|
||||
*/
|
||||
public function deleteRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Replace rows regardless if they exist or not.
|
||||
*
|
||||
* All rows should have a specified key for replace to work properly.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
*
|
||||
* @return array Returns both created and updated rows.
|
||||
*/
|
||||
public function replaceRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool;
|
||||
|
||||
/**
|
||||
* Get filesystem path for the collection or object storage.
|
||||
*
|
||||
* @param string|null $key Optional storage key.
|
||||
*
|
||||
* @return string Path in the filesystem. Can be URI.
|
||||
*/
|
||||
public function getStoragePath(string $key = null): string;
|
||||
|
||||
/**
|
||||
* Get filesystem path for the collection or object media.
|
||||
*
|
||||
* @param string|null $key Optional storage key.
|
||||
*
|
||||
* @return string Path in the filesystem. Can be URI.
|
||||
*/
|
||||
public function getMediaPath(string $key = null): string;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Storage;
|
||||
|
||||
use Grav\Common\File\CompiledJsonFile;
|
||||
use Grav\Common\File\CompiledMarkdownFile;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\File\Formatter\JsonFormatter;
|
||||
use Grav\Framework\File\Formatter\MarkdownFormatter;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class AbstractFilesystemStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
abstract class AbstractFilesystemStorage implements FlexStorageInterface
|
||||
{
|
||||
/** @var FileFormatterInterface */
|
||||
protected $dataFormatter;
|
||||
/** @var string */
|
||||
protected $keyField = 'storage_key';
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKey()
|
||||
*/
|
||||
public function hasKeys(array $keys): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
$list[$key] = $this->hasKey((string)$key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string
|
||||
{
|
||||
return $this->keyField;
|
||||
}
|
||||
|
||||
protected function initDataFormatter($formatter): void
|
||||
{
|
||||
// Initialize formatter.
|
||||
if (!\is_array($formatter)) {
|
||||
$formatter = ['class' => $formatter];
|
||||
}
|
||||
$formatterClassName = $formatter['class'] ?? JsonFormatter::class;
|
||||
$formatterOptions = $formatter['options'] ?? [];
|
||||
|
||||
$this->dataFormatter = new $formatterClassName($formatterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return null|string
|
||||
*/
|
||||
protected function detectDataFormatter(string $filename): ?string
|
||||
{
|
||||
if (preg_match('|(\.[a-z0-9]*)$|ui', $filename, $matches)) {
|
||||
switch ($matches[1]) {
|
||||
case '.json':
|
||||
return JsonFormatter::class;
|
||||
case '.yaml':
|
||||
return YamlFormatter::class;
|
||||
case '.md':
|
||||
return MarkdownFormatter::class;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return File
|
||||
*/
|
||||
protected function getFile(string $filename)
|
||||
{
|
||||
$filename = $this->resolvePath($filename);
|
||||
|
||||
switch ($this->dataFormatter->getDefaultFileExtension()) {
|
||||
case '.json':
|
||||
$file = CompiledJsonFile::instance($filename);
|
||||
break;
|
||||
case '.yaml':
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
break;
|
||||
case '.md':
|
||||
$file = CompiledMarkdownFile::instance($filename);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException('Unknown extension type ' . $this->dataFormatter->getDefaultFileExtension());
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function resolvePath(string $path): string
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if (!$locator->isStream($path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random, unique key for the row.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateKey(): string
|
||||
{
|
||||
return substr(hash('sha256', random_bytes(32)), 0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key is valid.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateKey(string $key): bool
|
||||
{
|
||||
return (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Storage;
|
||||
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
|
||||
/**
|
||||
* Class FileStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class FileStorage extends FolderStorage
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->dataPattern = '{FOLDER}/{KEY}';
|
||||
|
||||
if (!isset($options['formatter']) && isset($options['pattern'])) {
|
||||
$options['formatter'] = $this->detectDataFormatter($options['pattern']);
|
||||
}
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): string
|
||||
{
|
||||
return $key ? \dirname($this->getStoragePath($key)) . '/' . $key : $this->getStoragePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path, $this->dataFormatter->getDefaultFileExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
if (!file_exists($this->getStoragePath())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
|
||||
$iterator = new \FilesystemIterator($this->getStoragePath(), $flags);
|
||||
$list = [];
|
||||
/** @var \SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isFile() || !($key = $this->getKeyFromPath($filename)) || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$key] = [
|
||||
'storage_key' => $key,
|
||||
'storage_timestamp' => $info->getMTime()
|
||||
];
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Storage;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* Class FolderStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class FolderStorage extends AbstractFilesystemStorage
|
||||
{
|
||||
/** @var string */
|
||||
protected $dataFolder;
|
||||
/** @var string */
|
||||
protected $dataPattern = '{FOLDER}/{KEY}/item';
|
||||
/** @var bool */
|
||||
protected $prefixed;
|
||||
/** @var bool */
|
||||
protected $indexed;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (!isset($options['folder'])) {
|
||||
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
|
||||
}
|
||||
|
||||
$this->initDataFormatter($options['formatter'] ?? []);
|
||||
$this->initOptions($options);
|
||||
|
||||
// Make sure that the data folder exists.
|
||||
$folder = $this->resolvePath($this->dataFolder);
|
||||
if (!file_exists($folder)) {
|
||||
try {
|
||||
Folder::create($folder);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getExistingKeys()
|
||||
*/
|
||||
public function getExistingKeys(): array
|
||||
{
|
||||
return $this->buildIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKey()
|
||||
*/
|
||||
public function hasKey(string $key): bool
|
||||
{
|
||||
return $key && strpos($key, '@@') === false && file_exists($this->getPathFromKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::createRows()
|
||||
*/
|
||||
public function createRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
// Create new file and save it.
|
||||
$key = $this->getNewKey();
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->saveFile($file, $row);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::readRows()
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null === $row || (!\is_object($row) && !\is_array($row))) {
|
||||
// Only load rows which haven't been loaded before.
|
||||
$key = (string)$key;
|
||||
if (!$this->hasKey($key)) {
|
||||
$list[$key] = null;
|
||||
} else {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->loadFile($file);
|
||||
}
|
||||
if (null !== $fetched) {
|
||||
$fetched[$key] = $list[$key];
|
||||
}
|
||||
} else {
|
||||
// Keep the row if it has been loaded.
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::updateRows()
|
||||
*/
|
||||
public function updateRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if (!$this->hasKey($key)) {
|
||||
$list[$key] = null;
|
||||
} else {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->saveFile($file, $row);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::deleteRows()
|
||||
*/
|
||||
public function deleteRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if (!$this->hasKey($key)) {
|
||||
$list[$key] = null;
|
||||
} else {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->deleteFile($file);
|
||||
|
||||
$storage = $this->getStoragePath($key);
|
||||
$media = $this->getMediaPath($key);
|
||||
|
||||
$this->deleteFolder($storage, true);
|
||||
$media && $this->deleteFolder($media, true);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::replaceRows()
|
||||
*/
|
||||
public function replaceRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if (strpos($key, '@@')) {
|
||||
$key = $this->getNewKey();
|
||||
}
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->saveFile($file, $row);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::renameRow()
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool
|
||||
{
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->moveFolder($this->getMediaPath($src), $this->getMediaPath($dst));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getStoragePath()
|
||||
*/
|
||||
public function getStoragePath(string $key = null): string
|
||||
{
|
||||
if (null === $key) {
|
||||
$path = $this->dataFolder;
|
||||
} else {
|
||||
$path = sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): string
|
||||
{
|
||||
return null !== $key ? \dirname($this->getStoragePath($key)) : $this->getStoragePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filesystem path from the key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function getPathFromKey(string $key): string
|
||||
{
|
||||
return sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File $file
|
||||
* @return array|null
|
||||
*/
|
||||
protected function loadFile(File $file): ?array
|
||||
{
|
||||
if (!$file->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$content = (array)$file->content();
|
||||
if (isset($content[0])) {
|
||||
throw new \RuntimeException('Broken object file.');
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
$content = ['__error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File $file
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function saveFile(File $file, array $data): array
|
||||
{
|
||||
try {
|
||||
$file->save($data);
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($file->filename())) {
|
||||
$locator->clearCache($file->filename());
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex saveFile(%s): %s', $file->filename(), $e->getMessage()));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File $file
|
||||
* @return array|string
|
||||
*/
|
||||
protected function deleteFile(File $file)
|
||||
{
|
||||
try {
|
||||
$data = $file->content();
|
||||
$file->delete();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($file->filename())) {
|
||||
$locator->clearCache($file->filename());
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex deleteFile(%s): %s', $file->filename(), $e->getMessage()));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
protected function moveFolder(string $src, string $dst): bool
|
||||
{
|
||||
try {
|
||||
Folder::move($this->resolvePath($src), $this->resolvePath($dst));
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($src) || $locator->isStream($dst)) {
|
||||
$locator->clearCache();
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param bool $include_target
|
||||
* @return bool
|
||||
*/
|
||||
protected function deleteFolder(string $path, bool $include_target = false): bool
|
||||
{
|
||||
try {
|
||||
$success = Folder::delete($this->resolvePath($path), $include_target);
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
if ($locator->isStream($path)) {
|
||||
$locator->clearCache();
|
||||
}
|
||||
|
||||
return $success;
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key from the filesystem path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$path = $this->getStoragePath();
|
||||
if (!file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->prefixed) {
|
||||
$list = $this->buildPrefixedIndexFromFilesystem($path);
|
||||
} else {
|
||||
$list = $this->buildIndexFromFilesystem($path);
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
protected function buildIndexFromFilesystem($path)
|
||||
{
|
||||
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
|
||||
|
||||
$iterator = new \FilesystemIterator($path, $flags);
|
||||
$list = [];
|
||||
/** @var \SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->getKeyFromPath($filename);
|
||||
$filename = $this->getPathFromKey($key);
|
||||
$modified = is_file($filename) ? filemtime($filename) : null;
|
||||
if (null === $modified) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$key] = [
|
||||
'storage_key' => $key,
|
||||
'storage_timestamp' => $modified
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
protected function buildPrefixedIndexFromFilesystem($path)
|
||||
{
|
||||
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
|
||||
|
||||
$iterator = new \FilesystemIterator($path, $flags);
|
||||
$list = [];
|
||||
/** @var \SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $this->buildIndexFromFilesystem($filename);
|
||||
}
|
||||
|
||||
if (!$list) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \count($list) > 1 ? array_merge(...$list) : $list[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey(): string
|
||||
{
|
||||
// Make sure that the file doesn't exist.
|
||||
do {
|
||||
$key = $this->generateKey();
|
||||
} while (file_exists($this->getPathFromKey($key)));
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
protected function initOptions(array $options): void
|
||||
{
|
||||
$extension = $this->dataFormatter->getDefaultFileExtension();
|
||||
|
||||
/** @var string $pattern */
|
||||
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
|
||||
|
||||
$this->dataFolder = $options['folder'];
|
||||
$this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/'));
|
||||
$this->indexed = (bool)($options['indexed'] ?? false);
|
||||
$this->keyField = $options['key'] ?? 'storage_key';
|
||||
|
||||
$pattern = preg_replace(['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/'], ['%1$s', '%2$s', '%3$s'], $pattern);
|
||||
$this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Storage;
|
||||
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class SimpleStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class SimpleStorage extends AbstractFilesystemStorage
|
||||
{
|
||||
/** @var string */
|
||||
protected $dataFolder;
|
||||
/** @var string */
|
||||
protected $dataPattern;
|
||||
/** @var array */
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (!isset($options['folder'])) {
|
||||
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
|
||||
}
|
||||
|
||||
$formatter = $options['formatter'] ?? $this->detectDataFormatter($options['folder']);
|
||||
$this->initDataFormatter($formatter);
|
||||
|
||||
$extension = $this->dataFormatter->getDefaultFileExtension();
|
||||
$pattern = basename($options['folder']);
|
||||
|
||||
$this->dataPattern = basename($pattern, $extension) . $extension;
|
||||
$this->dataFolder = \dirname($options['folder']);
|
||||
|
||||
// Make sure that the data folder exists.
|
||||
if (!file_exists($this->dataFolder)) {
|
||||
try {
|
||||
Folder::create($this->dataFolder);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getExistingKeys()
|
||||
*/
|
||||
public function getExistingKeys(): array
|
||||
{
|
||||
return $this->buildIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKey()
|
||||
*/
|
||||
public function hasKey(string $key): bool
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
return $key && strpos($key, '@@') === false && isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::createRows()
|
||||
*/
|
||||
public function createRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = $this->getNewKey();
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::readRows()
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null === $row || (!\is_object($row) && !\is_array($row))) {
|
||||
// Only load rows which haven't been loaded before.
|
||||
$key = (string)$key;
|
||||
$list[$key] = $this->hasKey($key) ? $this->data[$key] : null;
|
||||
if (null !== $fetched) {
|
||||
$fetched[$key] = $list[$key];
|
||||
}
|
||||
} else {
|
||||
// Keep the row if it has been loaded.
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::updateRows()
|
||||
*/
|
||||
public function updateRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if ($this->hasKey($key)) {
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::deleteRows()
|
||||
*/
|
||||
public function deleteRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if ($this->hasKey($key)) {
|
||||
unset($this->data[$key]);
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::replaceRows()
|
||||
*/
|
||||
public function replaceRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (strpos($key, '@@')) {
|
||||
$key = $this->getNewKey();
|
||||
}
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::renameRow()
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Change single key in the array without changing the order or value.
|
||||
$keys = array_keys($this->data);
|
||||
$keys[array_search($src, $keys, true)] = $dst;
|
||||
|
||||
$data = array_combine($keys, $this->data);
|
||||
if (false === $data) {
|
||||
throw new \LogicException('Bad data');
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getStoragePath()
|
||||
*/
|
||||
public function getStoragePath(string $key = null): string
|
||||
{
|
||||
return $this->dataFolder . '/' . $this->dataPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): string
|
||||
{
|
||||
return sprintf('%s/%s/%s', $this->dataFolder, basename($this->dataPattern, $this->dataFormatter->getDefaultFileExtension()), $key);
|
||||
}
|
||||
|
||||
protected function save() : void
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $this->getFile($this->getStoragePath());
|
||||
$file->save($this->data);
|
||||
$file->free();
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException(sprintf('Flex save(): %s', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key from the filesystem path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$file = $this->getFile($this->getStoragePath());
|
||||
$modified = $file->modified();
|
||||
|
||||
$this->data = (array) $file->content();
|
||||
|
||||
$list = [];
|
||||
foreach ($this->data as $key => $info) {
|
||||
$list[$key] = [
|
||||
'storage_key' => $key,
|
||||
'storage_timestamp' => $modified
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey(): string
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
// Make sure that the key doesn't exist.
|
||||
do {
|
||||
$key = $this->generateKey();
|
||||
} while (isset($this->data[$key]));
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @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\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
|
||||
/**
|
||||
* Implements basic ACL
|
||||
*/
|
||||
trait FlexAuthorizeTrait
|
||||
{
|
||||
private $_authorize = '%s.flex-object.%s';
|
||||
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool
|
||||
{
|
||||
if (null === $user) {
|
||||
/** @var UserInterface $user */
|
||||
$user = Grav::instance()['user'] ?? null;
|
||||
}
|
||||
|
||||
return $user && ($this->isAuthorizedAction($user, $action, $scope) || $this->isAuthorizedSuperAdmin($user));
|
||||
}
|
||||
|
||||
protected function isAuthorizedSuperAdmin(UserInterface $user): bool
|
||||
{
|
||||
return $user->authorize('admin.super');
|
||||
}
|
||||
|
||||
protected function isAuthorizedAction(UserInterface $user, string $action, string $scope = null) : bool
|
||||
{
|
||||
$scope = $scope ?? isset(Grav::instance()['admin']) ? 'admin' : 'site';
|
||||
|
||||
if ($action === 'save' && $this instanceof FlexObjectInterface) {
|
||||
$action = $this->exists() ? 'update' : 'create';
|
||||
}
|
||||
|
||||
$directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory();
|
||||
$config = $directory->getConfig();
|
||||
$allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true;
|
||||
|
||||
return $allowed && $user->authorize(sprintf($this->_authorize, $scope, $action));
|
||||
}
|
||||
|
||||
protected function setAuthorizeRule(string $authorize) : void
|
||||
{
|
||||
$this->_authorize = $authorize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Framework\Flex\Traits;
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
use Grav\Common\Cache;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
||||
use Grav\Common\Media\Traits\MediaTrait;
|
||||
use Grav\Common\Page\Medium\AbstractMedia;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Form\FormFlashFile;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Implements Grav Page content and header manipulation methods.
|
||||
*/
|
||||
trait FlexMediaTrait
|
||||
{
|
||||
use MediaTrait {
|
||||
MediaTrait::getMedia as protected getExistingMedia;
|
||||
}
|
||||
|
||||
protected $_uploads;
|
||||
|
||||
public function __debugInfo()
|
||||
{
|
||||
return parent::__debugInfo() + [
|
||||
'uploads:private' => $this->getUpdatedMedia()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStorageFolder()
|
||||
{
|
||||
return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaFolder()
|
||||
{
|
||||
return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MediaCollectionInterface
|
||||
*/
|
||||
public function getMedia()
|
||||
{
|
||||
if ($this->media === null) {
|
||||
/** @var AbstractMedia $media */
|
||||
$media = $this->getExistingMedia();
|
||||
|
||||
// Include uploaded media to the object media.
|
||||
/** @var FormFlashFile $upload */
|
||||
foreach ($this->getUpdatedMedia() as $filename => $upload) {
|
||||
// Just make sure we do not include removed or moved media.
|
||||
if ($upload && $upload->getError() === \UPLOAD_ERR_OK && !$upload->isMoved()) {
|
||||
$media->add($filename, MediumFactory::fromUploadedFile($upload));
|
||||
}
|
||||
}
|
||||
|
||||
$media->setTimestamps();
|
||||
}
|
||||
|
||||
return $this->media;
|
||||
}
|
||||
|
||||
public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$language = $grav['language'];
|
||||
|
||||
switch ($uploadedFile->getError()) {
|
||||
case UPLOAD_ERR_OK:
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
if ($uploadedFile instanceof FormFlashFile) {
|
||||
break;
|
||||
}
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400);
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400);
|
||||
default:
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
|
||||
}
|
||||
|
||||
$filename = $uploadedFile->getClientFilename();
|
||||
|
||||
if (!Utils::checkFilename($filename)) {
|
||||
throw new RuntimeException(sprintf($language->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filename, 'Bad filename'), 400);
|
||||
}
|
||||
|
||||
$grav_limit = Utils::getUploadLimit();
|
||||
if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
|
||||
}
|
||||
|
||||
$this->checkMediaFilename($filename);
|
||||
}
|
||||
|
||||
public function checkMediaFilename(string $filename)
|
||||
{
|
||||
// Check the file extension.
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
// If not a supported type, return
|
||||
if (!$extension || !$config->get("media.types.{$extension}")) {
|
||||
$language = $grav['language'];
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null): void
|
||||
{
|
||||
$this->checkUploadedMediaFile($uploadedFile);
|
||||
|
||||
if ($filename) {
|
||||
$this->checkMediaFilename(basename($filename));
|
||||
} else {
|
||||
$filename = $uploadedFile->getClientFilename();
|
||||
}
|
||||
|
||||
$media = $this->getMedia();
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$path = $media->getPath();
|
||||
if (!$path) {
|
||||
$language = $grav['language'];
|
||||
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
|
||||
}
|
||||
|
||||
if ($locator->isStream($path)) {
|
||||
$path = $locator->findResource($path, true, true);
|
||||
$locator->clearCache($path);
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload it
|
||||
$filepath = sprintf('%s/%s', $path, $filename);
|
||||
Folder::create(\dirname($filepath));
|
||||
if ($uploadedFile instanceof FormFlashFile) {
|
||||
$metadata = $uploadedFile->getMetaData();
|
||||
if ($metadata) {
|
||||
$file = YamlFile::instance($filepath . '.meta.yaml');
|
||||
$file->save(['upload' => $metadata]);
|
||||
}
|
||||
if ($uploadedFile->getError() === \UPLOAD_ERR_OK) {
|
||||
$uploadedFile->moveTo($filepath);
|
||||
} elseif (!file_exists($filepath) && $pos = strpos($filename, '/')) {
|
||||
$origpath = sprintf('%s/%s', $path, substr($filename, $pos));
|
||||
if (file_exists($origpath)) {
|
||||
copy($origpath, $filepath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$uploadedFile->moveTo($filepath);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$language = $grav['language'];
|
||||
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
|
||||
}
|
||||
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
public function deleteMediaFile(string $filename): void
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$language = $grav['language'];
|
||||
|
||||
$basename = basename($filename);
|
||||
$dirname = dirname($filename);
|
||||
$dirname = $dirname === '.' ? '' : '/' . $dirname;
|
||||
|
||||
if (!Utils::checkFilename($basename)) {
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': Bad filename: ' . $filename, 400);
|
||||
}
|
||||
|
||||
$media = $this->getMedia();
|
||||
$path = $media->getPath();
|
||||
if (!$path) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$targetPath = $path . '/' . $dirname;
|
||||
$targetFile = $path . '/' . $filename;
|
||||
if ($locator->isStream($targetFile)) {
|
||||
$targetPath = $locator->findResource($targetPath, true, true);
|
||||
$targetFile = $locator->findResource($targetFile, true, true);
|
||||
$locator->clearCache($targetPath);
|
||||
$locator->clearCache($targetFile);
|
||||
}
|
||||
|
||||
$fileParts = pathinfo($basename);
|
||||
|
||||
if (!file_exists($targetPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (file_exists($targetFile)) {
|
||||
$result = unlink($targetFile);
|
||||
if (!$result) {
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Extra Files
|
||||
foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) {
|
||||
$preg_name = preg_quote($fileParts['filename'], '`');
|
||||
$preg_ext =preg_quote($fileParts['extension'], '`');
|
||||
$preg_filename = preg_quote($basename, '`');
|
||||
|
||||
if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) {
|
||||
$testPath = $targetPath . '/' . $file;
|
||||
if ($locator->isStream($testPath)) {
|
||||
$testPath = $locator->findResource($testPath, true, true);
|
||||
$locator->clearCache($testPath);
|
||||
}
|
||||
|
||||
if (is_file($testPath)) {
|
||||
$result = unlink($testPath);
|
||||
if (!$result) {
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
*/
|
||||
protected function setUpdatedMedia(array $files): void
|
||||
{
|
||||
$list = [];
|
||||
foreach ($files as $field => $group) {
|
||||
if ($field === '' || \strpos($field, '/', true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($group as $filename => $file) {
|
||||
$list[$filename] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
$this->_uploads = $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getUpdatedMedia(): array
|
||||
{
|
||||
return $this->_uploads ?? [];
|
||||
}
|
||||
|
||||
protected function saveUpdatedMedia(): void
|
||||
{
|
||||
/**
|
||||
* @var string $filename
|
||||
* @var UploadedFileInterface $file
|
||||
*/
|
||||
foreach ($this->getUpdatedMedia() as $filename => $file) {
|
||||
if ($file) {
|
||||
$this->uploadMediaFile($file, $filename);
|
||||
} else {
|
||||
$this->deleteMediaFile($filename);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setUpdatedMedia([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Medium|null
|
||||
*/
|
||||
protected function createMedium($uri)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri;
|
||||
|
||||
return $file && file_exists($file) ? MediumFactory::fromFile($file) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cache
|
||||
*/
|
||||
protected function getMediaCache()
|
||||
{
|
||||
return $this->getCache('object');
|
||||
}
|
||||
|
||||
protected function offsetLoad_media()
|
||||
{
|
||||
return $this->getMedia();
|
||||
}
|
||||
|
||||
protected function offsetSerialize_media()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract public function getFlexDirectory(): FlexDirectory;
|
||||
|
||||
abstract public function getStorageKey();
|
||||
}
|
||||
Reference in New Issue
Block a user