mirror of
https://github.com/getgrav/grav.git
synced 2026-04-05 04:08:49 +02:00
Added Grav\Framework\Flex classes
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
* Added `Grav\Framework\File` classes for handling YAML, Markdown, JSON, INI and PHP serialized files
|
||||
* Added `Grav\Framework\Collection\AbstractIndexCollection` class
|
||||
* Added `Grav\Framework\Object\ObjectIndex` class
|
||||
* Added `Grav\Framework\Flex` classes
|
||||
|
||||
# v1.5.2
|
||||
## mm/dd/2018
|
||||
|
||||
364
system/src/Grav/Framework/Flex/FlexCollection.php
Normal file
364
system/src/Grav/Framework/Flex/FlexCollection.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
use Grav\Framework\Object\ObjectCollection;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* Class FlexCollection
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexCollection extends ObjectCollection implements FlexCollectionInterface
|
||||
{
|
||||
/** @var FlexDirectory */
|
||||
private $flexDirectory;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods()
|
||||
{
|
||||
return [
|
||||
'getTypePrefix' => true,
|
||||
'getType' => true,
|
||||
'getFlexDirectory' => true,
|
||||
'getCacheKey' => true,
|
||||
'getCacheChecksum' => true,
|
||||
'getTimestamp' => true,
|
||||
'hasProperty' => true,
|
||||
'getProperty' => true,
|
||||
'hasNestedProperty' => true,
|
||||
'getNestedProperty' => true,
|
||||
'orderBy' => true,
|
||||
|
||||
'authorize' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param FlexDirectory|null $flexDirectory
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $elements = [], FlexDirectory $flexDirectory = null)
|
||||
{
|
||||
parent::__construct($elements);
|
||||
|
||||
if ($flexDirectory) {
|
||||
$this->setFlexDirectory($flexDirectory)->setKey($flexDirectory->getType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function createFrom(array $elements)
|
||||
{
|
||||
return new static($elements, $this->flexDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix()
|
||||
{
|
||||
return 'c.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $prefix
|
||||
* @return string
|
||||
*/
|
||||
public function getType($prefix = true)
|
||||
{
|
||||
$type = $prefix ? $this->getTypePrefix() : '';
|
||||
|
||||
return $type . $this->flexDirectory->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @param array $context
|
||||
* @return HtmlBlock
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
public function render($layout = null, array $context = [])
|
||||
{
|
||||
if (null === $layout) {
|
||||
$layout = 'default';
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->startTimer('flex-collection-' . ($debugKey = uniqid($this->getType(false), false)), 'Render Collection ' . $this->getType(false));
|
||||
|
||||
$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->flexDirectory->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);
|
||||
|
||||
$grav->fireEvent('onFlexCollectionRender', new Event([
|
||||
'collection' => $this,
|
||||
'layout' => &$layout,
|
||||
'context' => &$context
|
||||
]));
|
||||
|
||||
$output = $this->getTemplate($layout)->render(
|
||||
['grav' => $grav, 'block' => $block, 'collection' => $this, 'layout' => $layout] + $context
|
||||
);
|
||||
|
||||
$block->setContent($output);
|
||||
|
||||
try {
|
||||
$cache && $cache->set($key, $block->toArray());
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-collection-' . $debugKey);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $type
|
||||
* @return $this
|
||||
*/
|
||||
public function setFlexDirectory(FlexDirectory $type)
|
||||
{
|
||||
$this->flexDirectory = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getFlexDirectory() : FlexDirectory
|
||||
{
|
||||
return $this->flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey()
|
||||
{
|
||||
return $this->getType(true) . '.' . sha1(json_encode($this->call('getKey')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheChecksum()
|
||||
{
|
||||
return sha1(json_encode($this->getTimestamps()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getTimestamps()
|
||||
{
|
||||
return $this->call('getTimestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null)
|
||||
{
|
||||
$list = $this->call('authorize', [$action, $scope]);
|
||||
$list = \array_filter($list);
|
||||
|
||||
return $this->select(array_keys($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @param string $field
|
||||
* @return object|null
|
||||
*/
|
||||
public function find($value, $field = 'id')
|
||||
{
|
||||
if ($value) foreach ($this as $element) {
|
||||
if (strtolower($element->getProperty($field)) === strtolower($value)) {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ordering
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function orderBy(array $ordering)
|
||||
{
|
||||
$criteria = Criteria::create()->orderBy($ordering);
|
||||
|
||||
return $this->matching($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $start
|
||||
* @param int|null $limit
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function limit($start, $limit = null)
|
||||
{
|
||||
return $this->createFrom($this->slice($start, $limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select items from collection.
|
||||
*
|
||||
* Collection is returned in the order of $keys given to the function.
|
||||
*
|
||||
* @param array $keys
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function select(array $keys)
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
if ($this->containsKey($key)) {
|
||||
$list[$key] = $this->get($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-select items from collection.
|
||||
*
|
||||
* Collection is returned in the order of $keys given to the function.
|
||||
*
|
||||
* @param array $keys
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function unselect(array $keys)
|
||||
{
|
||||
return $this->select(array_diff($this->getKeys(), $keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$elements = [];
|
||||
|
||||
/**
|
||||
* @var string $key
|
||||
* @var FlexObject $object
|
||||
*/
|
||||
foreach ($this->getElements() as $key => $object) {
|
||||
$elements[$key] = $object->jsonSerialize();
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return \Twig_Template
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
try {
|
||||
return $twig->twig()->resolveTemplate(["flex-objects/layouts/{$this->getType(false)}/collection/{$layout}.html.twig"]);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
return $twig->twig()->resolveTemplate(["flex-objects/layouts/404.html.twig"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return FlexDirectory
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function getRelatedDirectory($type)
|
||||
{
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
578
system/src/Grav/Framework/Flex/FlexDirectory.php
Normal file
578
system/src/Grav/Framework/Flex/FlexDirectory.php
Normal file
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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\Collection\CollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use Grav\Framework\Flex\Storage\SimpleStorage;
|
||||
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
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 FlexIndex */
|
||||
protected $index;
|
||||
/** @var FlexCollection */
|
||||
protected $collection;
|
||||
/** @var bool */
|
||||
protected $enabled;
|
||||
/** @var array */
|
||||
protected $defaults;
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
/** @var object */
|
||||
protected $storage;
|
||||
/** @var CacheInterface */
|
||||
protected $cache;
|
||||
|
||||
protected $objectClassName;
|
||||
protected $collectionClassName;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function getType() : string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() : string
|
||||
{
|
||||
return $this->getBlueprintInternal()->get('title', ucfirst($this->getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$blueprint = $this->getBlueprintInternal($type, $context);
|
||||
|
||||
if (empty($this->blueprints_init[$type])) {
|
||||
$this->blueprints_init[$type] = true;
|
||||
|
||||
$blueprint->init();
|
||||
if (empty($blueprint->fields())) {
|
||||
throw new RuntimeException(sprintf('Flex: Blueprint for %s is missing', $this->type));
|
||||
}
|
||||
}
|
||||
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBlueprintFile() : string
|
||||
{
|
||||
return $this->blueprint_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $keys Array of keys.
|
||||
* @return FlexIndex|FlexCollection
|
||||
*/
|
||||
public function getCollection(array $keys = null) : CollectionInterface
|
||||
{
|
||||
$index = clone $this->getIndex($keys);
|
||||
|
||||
if (!Utils::isAdminPlugin()) {
|
||||
$filters = (array)$this->getConfig('site.filter', []);
|
||||
foreach ($filters as $filter) {
|
||||
$index = $index->{$filter}();
|
||||
}
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return FlexObject|null
|
||||
*/
|
||||
public function getObject($key) : ?FlexObject
|
||||
{
|
||||
return $this->getCollection()->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string|null $key
|
||||
* @param bool $isFullUpdate
|
||||
* @return FlexObject
|
||||
*/
|
||||
public function update(array $data, string $key = null, bool $isFullUpdate = false) : FlexObject
|
||||
{
|
||||
$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->triggerEvent('onSave')->prepareStorage()]);
|
||||
} else {
|
||||
$rows = $storage->createRows([$object->triggerEvent('onSave')->prepareStorage()]);
|
||||
}
|
||||
} else {
|
||||
$oldKey = $object->getStorageKey();
|
||||
$object->update($data, $isFullUpdate);
|
||||
$newKey = $object->getStorageKey();
|
||||
|
||||
if ($oldKey !== $newKey) {
|
||||
$object->triggerEvent('move');
|
||||
$storage->renameRow($oldKey, $newKey);
|
||||
// TODO: media support.
|
||||
}
|
||||
|
||||
$object->save();
|
||||
//$rows = $storage->updateRows([$newKey => $object->triggerEvent('onSave')->prepareStorage()]);
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
/** @var FlexObject $class */
|
||||
//$class = $this->getObjectClass();
|
||||
//
|
||||
//$row = $object;
|
||||
//$index = $class::createIndex([key($rows) => time()]);
|
||||
//$object = $this->createObject($row, key($index), false);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return FlexObject|null
|
||||
*/
|
||||
public function remove(string $key) : ?FlexObject
|
||||
{
|
||||
$object = null !== $key ? $this->getIndex()->get($key) : null;
|
||||
if (!$object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->getStorage()->deleteRows([$object->getStorageKey() => $object->triggerEvent('onRemove')->prepareStorage()]);
|
||||
|
||||
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|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null) : CacheInterface
|
||||
{
|
||||
$namespace = $namespace ?: 'index';
|
||||
|
||||
if (!isset($this->cache[$namespace])) {
|
||||
try {
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Cache $gravCache */
|
||||
$gravCache = $grav['cache'];
|
||||
$config = $this->getConfig('cache.' . $namespace);
|
||||
if (empty($config['enabled'])) {
|
||||
throw new \RuntimeException(sprintf('Flex: %s %s cache not enabled', $this->type, $namespace));
|
||||
}
|
||||
$timeout = $config['timeout'] ?? 60;
|
||||
|
||||
$key = $gravCache->getKey();
|
||||
if (Utils::isAdminPlugin()) {
|
||||
$key = substr($key, 0, -1);
|
||||
}
|
||||
$this->cache[$namespace] = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getType() . $key, $timeout);
|
||||
} catch (\Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$this->cache[$namespace] = new MemoryCache('flex-objects-' . $this->getType());
|
||||
}
|
||||
|
||||
// Disable cache key validation.
|
||||
$this->cache[$namespace]->setValidation(false);
|
||||
}
|
||||
|
||||
return $this->cache[$namespace];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function clearCache() : self
|
||||
{
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug');
|
||||
|
||||
$this->getCache('index')->clear();
|
||||
$this->getCache('object')->clear();
|
||||
$this->getCache('render')->clear();
|
||||
|
||||
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 (!$this->storage) {
|
||||
$this->storage = $this->createStorage();
|
||||
}
|
||||
|
||||
return $this->storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $keys Array of keys.
|
||||
* @return FlexIndex|FlexCollection
|
||||
* @internal
|
||||
*/
|
||||
public function getIndex(array $keys = null) : CollectionInterface
|
||||
{
|
||||
$index = clone $this->loadIndex();
|
||||
|
||||
if (null !== $keys) {
|
||||
$index = $index->select($keys);
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string $key
|
||||
* @param bool $validate
|
||||
* @return FlexObject
|
||||
*/
|
||||
public function createObject(array $data, string $key, bool $validate = false) : FlexObject
|
||||
{
|
||||
$className = $this->objectClassName ?: $this->getObjectClass();
|
||||
|
||||
return new $className($data, $key, $this, $validate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function createCollection(array $entries) : FlexCollection
|
||||
{
|
||||
$className = $this->collectionClassName ?: $this->getCollectionClass();
|
||||
|
||||
return new $className($entries, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getObjectClass() : string
|
||||
{
|
||||
if (!$this->objectClassName) {
|
||||
$this->objectClassName = $this->getConfig('data.object', 'Grav\\Plugin\\FlexObjects\\FlexObject');
|
||||
}
|
||||
return $this->objectClassName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCollectionClass() : string
|
||||
{
|
||||
if (!$this->collectionClassName) {
|
||||
$this->collectionClassName = $this->getConfig('data.collection', 'Grav\\Plugin\\FlexObjects\\FlexCollection');
|
||||
}
|
||||
return $this->collectionClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return FlexCollection
|
||||
*/
|
||||
public function loadCollection(array $entries) : FlexCollection
|
||||
{
|
||||
return $this->createCollection($this->loadObjects($entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return FlexObject[]
|
||||
*/
|
||||
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 = \is_array($value) ? $value[0] : $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 {
|
||||
$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;
|
||||
}
|
||||
$row += [
|
||||
'storage_key' => $storageKey,
|
||||
'storage_timestamp' => $entries[$key][1] ?? $entries[$key],
|
||||
];
|
||||
$key = $keys[$storageKey];
|
||||
$object = $this->createObject($row, $key, false);
|
||||
$list[$key] = $object;
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-objects');
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $context
|
||||
* @return Blueprint
|
||||
*/
|
||||
protected function getBlueprintInternal(string $type = '', string $context = '') : Blueprint
|
||||
{
|
||||
if (!isset($this->blueprints[$type])) {
|
||||
if (!file_exists($this->blueprint_file)) {
|
||||
throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type));
|
||||
}
|
||||
$blueprint = new Blueprint($this->blueprint_file);
|
||||
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] = $blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprints[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 FlexIndex|FlexCollection
|
||||
*/
|
||||
protected function loadIndex() : CollectionInterface
|
||||
{
|
||||
static $i = 0;
|
||||
|
||||
if (null === $this->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 FlexObject $className */
|
||||
$className = $this->getObjectClass();
|
||||
$keys = $className::createIndex($storage->getExistingKeys());
|
||||
$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 = new FlexIndex($keys, $this);
|
||||
$this->index = $this->index->orderBy($this->getConfig('data.ordering', []));
|
||||
|
||||
$debugger->stopTimer('flex-keys-' . $this->type . $j);
|
||||
}
|
||||
|
||||
return $this->index;
|
||||
}
|
||||
}
|
||||
316
system/src/Grav/Framework/Flex/FlexForm.php
Normal file
316
system/src/Grav/Framework/Flex/FlexForm.php
Normal file
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Data\ValidationException;
|
||||
use Grav\Common\Grav;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Class FlexForm
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexForm implements \Serializable
|
||||
{
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var bool */
|
||||
private $submitted;
|
||||
|
||||
/** @var string[] */
|
||||
private $errors;
|
||||
|
||||
/** @var Data */
|
||||
private $data;
|
||||
|
||||
/** @var UploadedFileInterface[] */
|
||||
private $files;
|
||||
|
||||
/** @var FlexObject */
|
||||
private $object;
|
||||
|
||||
/**
|
||||
* FlexForm constructor.
|
||||
* @param string $name
|
||||
* @param FlexObject|null $object
|
||||
*/
|
||||
public function __construct(string $name, FlexObject $object = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($object) {
|
||||
$this->setObject($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName() : string
|
||||
{
|
||||
$object = $this->object;
|
||||
|
||||
return "flex-{$object->getType(false)}-{$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAction() : string
|
||||
{
|
||||
// TODO:
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getButtons() : array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'type' => 'submit',
|
||||
'value' => 'Save'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data
|
||||
*/
|
||||
public function getData() : Data
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->data = new Data($this->getObject()->jsonSerialize());
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the form.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(string $name)
|
||||
{
|
||||
return $this->getData()->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UploadedFileInterface[]
|
||||
*/
|
||||
public function getFiles() : array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method clones the object.
|
||||
*
|
||||
* @param FlexObject $object
|
||||
* @return $this
|
||||
*/
|
||||
public function setObject(FlexObject $object) : self
|
||||
{
|
||||
$this->object = clone $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexObject
|
||||
*/
|
||||
public function getObject() : FlexObject
|
||||
{
|
||||
if (!$this->object) {
|
||||
throw new \RuntimeException('FlexForm: Object is not defined');
|
||||
}
|
||||
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return $this
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request) : self
|
||||
{
|
||||
try {
|
||||
$method = $request->getMethod();
|
||||
if (!\in_array($method, ['PUT', 'POST', 'PATCH'])) {
|
||||
throw new \RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
$files = $request->getUploadedFiles();
|
||||
|
||||
$this->submit($data, $files);
|
||||
} catch (\Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid() : bool
|
||||
{
|
||||
return !$this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors() : array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubmitted() : bool
|
||||
{
|
||||
return $this->submitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param UploadedFileInterface[] $files
|
||||
* @return $this
|
||||
*/
|
||||
public function submit(array $data, array $files = null) : self
|
||||
{
|
||||
try {
|
||||
if ($this->isSubmitted()) {
|
||||
throw new \RuntimeException('Form has already been submitted');
|
||||
}
|
||||
|
||||
$this->data = new Data($data);
|
||||
$this->files = $files ?? [];
|
||||
$this->submitted = true;
|
||||
|
||||
$this->checkUploads($files);
|
||||
|
||||
$object = clone $this->object;
|
||||
$object->update($this->data->toArray());
|
||||
/*
|
||||
if (method_exists($object, 'upload')) {
|
||||
$object->upload($this->files);
|
||||
}
|
||||
$object->save();
|
||||
*/
|
||||
|
||||
$this->object = $object;
|
||||
$this->valid = true;
|
||||
} catch (ValidationException $e) {
|
||||
$this->errors = $e->getMessages();
|
||||
} catch (\Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function reset() : self
|
||||
{
|
||||
$this->data = null;
|
||||
$this->files = [];
|
||||
$this->errors = [];
|
||||
$this->submitted = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields() : array
|
||||
{
|
||||
return $this->getObject()->getBlueprint()->fields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Serializable::serialize().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize() : string
|
||||
{
|
||||
$data = [
|
||||
'name' => $this->name,
|
||||
'data' => $this->data,
|
||||
'files' => $this->files,
|
||||
'errors' => $this->errors,
|
||||
'submitted' => $this->submitted,
|
||||
'object' => $this->object,
|
||||
];
|
||||
|
||||
return serialize($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Serializable::unserialize().
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function unserialize($data) : void
|
||||
{
|
||||
$data = unserialize($data, ['allowed_classes' => [FlexObject::class]]);
|
||||
|
||||
$this->name = $data['name'];
|
||||
$this->data = $data['data'];
|
||||
$this->files = $data['files'];
|
||||
$this->errors = $data['errors'];
|
||||
$this->submitted = $data['submitted'];
|
||||
$this->object = $data['object'];
|
||||
}
|
||||
|
||||
protected function checkUploads(array $files)
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
if ($file instanceof UploadedFileInterface) {
|
||||
$this->checkUpload($file);
|
||||
} else {
|
||||
$this->checkUploads($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function checkUpload(UploadedFileInterface $file) : void
|
||||
{
|
||||
// Handle bad filenames.
|
||||
$filename = $file->getClientFilename();
|
||||
if (strtr($filename, "\t\n\r\0\x0b", '_____') !== $filename
|
||||
|| rtrim($filename, '. ') !== $filename
|
||||
|| preg_match('|\.php|', $filename)) {
|
||||
$grav = Grav::instance();
|
||||
throw new \RuntimeException(
|
||||
sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
299
system/src/Grav/Framework/Flex/FlexIndex.php
Normal file
299
system/src/Grav/Framework/Flex/FlexIndex.php
Normal file
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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\Grav;
|
||||
use Grav\Framework\Collection\CollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectInterface;
|
||||
use Grav\Framework\Object\ObjectIndex;
|
||||
use PSR\SimpleCache\InvalidArgumentException;
|
||||
|
||||
class FlexIndex extends ObjectIndex implements FlexCollectionInterface
|
||||
{
|
||||
/** @var FlexDirectory */
|
||||
private $flexDirectory;
|
||||
|
||||
/**
|
||||
* Initializes a new FlexIndex.
|
||||
*
|
||||
* @param array $entries
|
||||
* @param FlexDirectory $flexDirectory
|
||||
*/
|
||||
public function __construct(array $entries, FlexDirectory $flexDirectory)
|
||||
{
|
||||
parent::__construct($entries);
|
||||
|
||||
$this->flexDirectory = $flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getFlexDirectory() : FlexDirectory
|
||||
{
|
||||
return $this->flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $prefix
|
||||
* @return string
|
||||
*/
|
||||
public function getType($prefix = true)
|
||||
{
|
||||
$type = $prefix ? $this->getTypePrefix() : '';
|
||||
|
||||
return $type . $this->flexDirectory->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getStorageKeys()
|
||||
{
|
||||
// Get storage keys for the objects.
|
||||
$keys = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$keys[\is_array($value) ? $value[0] : $key] = $key;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getTimestamps()
|
||||
{
|
||||
// Get storage keys for the objects.
|
||||
$timestamps = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$timestamps[$key] = \is_array($value) ? $value[1] : $value;
|
||||
}
|
||||
|
||||
return $timestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey()
|
||||
{
|
||||
return $this->getType(true) . '.' . sha1(json_encode($this->getKeys()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheChecksum()
|
||||
{
|
||||
return sha1($this->getCacheKey() . json_encode($this->getTimestamps()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $orderings
|
||||
* @return FlexIndex|FlexCollection
|
||||
*/
|
||||
public function orderBy(array $orderings)
|
||||
{
|
||||
if (!$orderings) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Check if ordering needs to load the objects.
|
||||
if (array_diff_key($orderings, ['key' => true, 'storage_key' => true, 'timestamp' => true])) {
|
||||
return $this->__call('orderBy', [$orderings]);
|
||||
}
|
||||
|
||||
// Ordering can be done by using index only.
|
||||
$previous = null;
|
||||
foreach (array_reverse($orderings) as $field => $ordering) {
|
||||
switch ($field) {
|
||||
case 'key':
|
||||
$keys = $this->getKeys();
|
||||
$search = array_combine($keys, $keys);
|
||||
break;
|
||||
case 'storage_key':
|
||||
$search = array_flip($this->getStorageKeys());
|
||||
break;
|
||||
case 'timestamp':
|
||||
$search = $this->getTimestamps();
|
||||
break;
|
||||
default:
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if (!empty($cachedMethods[$name])) {
|
||||
$key = $this->getType(true) . '.' . sha1($name . '.' . json_encode($arguments) . $this->getCacheKey());
|
||||
|
||||
$cache = $this->flexDirectory->getCache('object');
|
||||
|
||||
$test = new \stdClass;
|
||||
try {
|
||||
$result = $cache->get($key, $test);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$result = $test;
|
||||
}
|
||||
|
||||
if ($result === $test) {
|
||||
$result = $this->loadCollection()->{$name}(...$arguments);
|
||||
|
||||
try {
|
||||
// If flex collection is returned, convert it back to flex index.
|
||||
if ($result instanceof FlexCollection) {
|
||||
$cached = $result->getFlexDirectory()->getIndex($result->getKeys());
|
||||
} else {
|
||||
$cached = $result;
|
||||
}
|
||||
|
||||
$cache->set($key, $cached);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
|
||||
// TODO: log error.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$collection = $this->loadCollection();
|
||||
$result = $collection->{$name}(...$arguments);
|
||||
$class = \get_class($collection);
|
||||
$debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize(['type' => $this->getType(false), 'entries' => $this->getEntries()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
|
||||
$this->flexDirectory = Grav::instance()['flex_objects']->getDirectory($data['type']);
|
||||
$this->setEntries($data['entries']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param array $indexes
|
||||
* @return static
|
||||
*/
|
||||
protected function createFrom(array $entries)
|
||||
{
|
||||
return new static($entries, $this->flexDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
582
system/src/Grav/Framework/Flex/FlexObject.php
Normal file
582
system/src/Grav/Framework/Flex/FlexObject.php
Normal file
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Grav\Common\Data\ValidationException;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
|
||||
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\Property\LazyPropertyTrait;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* 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 string */
|
||||
private $storageKey;
|
||||
/** @var int */
|
||||
private $timestamp = 0;
|
||||
/** @var FlexForm[] */
|
||||
private $forms = [];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods()
|
||||
{
|
||||
return [
|
||||
'getTypePrefix' => true,
|
||||
'getType' => true,
|
||||
'getFlexDirectory' => true,
|
||||
'getCacheKey' => true,
|
||||
'getCacheChecksum' => true,
|
||||
'getTimestamp' => true,
|
||||
'value' => true,
|
||||
'exists' => true,
|
||||
'hasProperty' => true,
|
||||
'getProperty' => true,
|
||||
|
||||
// FlexAclTrait
|
||||
'authorize' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $index
|
||||
* @return array
|
||||
*/
|
||||
public static function createIndex(array $index)
|
||||
{
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param string $key
|
||||
* @param FlexDirectory $flexDirectory
|
||||
* @param bool $validate
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function __construct(array $elements, $key, FlexDirectory $flexDirectory, $validate = false)
|
||||
{
|
||||
$this->flexDirectory = $flexDirectory;
|
||||
|
||||
if ($validate) {
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint();
|
||||
|
||||
$blueprint->validate($elements);
|
||||
|
||||
$elements = $blueprint->filter($elements);
|
||||
}
|
||||
|
||||
$this->filterElements($elements);
|
||||
|
||||
$this->objectConstruct($elements, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param bool $isFullUpdate
|
||||
* @return $this
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(array $data, $isFullUpdate = false)
|
||||
{
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint();
|
||||
|
||||
if (!$isFullUpdate) {
|
||||
$elements = $this->getElements();
|
||||
$data = $blueprint->mergeData($elements, $data);
|
||||
}
|
||||
|
||||
$blueprint->validate($data + ['storage_key' => $this->getStorageKey()]);
|
||||
$data = $blueprint->filter($data);
|
||||
|
||||
$this->filterElements($data);
|
||||
$this->setElements($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix()
|
||||
{
|
||||
return 'o.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $prefix
|
||||
* @return string
|
||||
*/
|
||||
public function getType($prefix = true)
|
||||
{
|
||||
$type = $prefix ? $this->getTypePrefix() : '';
|
||||
|
||||
return $type . $this->flexDirectory->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getFlexDirectory() : FlexDirectory
|
||||
{
|
||||
return $this->flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return FlexForm
|
||||
*/
|
||||
public function getForm($name = 'default')
|
||||
{
|
||||
if (!isset($this->forms[$name])) {
|
||||
$this->forms[$name] = new FlexForm($name, $this);
|
||||
}
|
||||
|
||||
return $this->forms[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Grav\Common\Data\Blueprint
|
||||
*/
|
||||
public function getBlueprint()
|
||||
{
|
||||
return $this->flexDirectory->getBlueprint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of getBlueprint()
|
||||
*
|
||||
* @return \Grav\Common\Data\Blueprint
|
||||
* @deprecated Admin compatibility
|
||||
*/
|
||||
public function blueprints()
|
||||
{
|
||||
return $this->getBlueprint();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey()
|
||||
{
|
||||
return $this->getType(true) .'.'. $this->getStorageKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCacheChecksum()
|
||||
{
|
||||
return $this->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStorageKey()
|
||||
{
|
||||
return $this->storageKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return $this
|
||||
*/
|
||||
public function setStorageKey($key = null)
|
||||
{
|
||||
$this->storageKey = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimestamp() : int
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $timestamp
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimestamp($timestamp = null)
|
||||
{
|
||||
$this->timestamp = $timestamp ?? time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @param array $context
|
||||
* @return HtmlBlock
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
public function render($layout = null, array $context = [])
|
||||
{
|
||||
if (null === $layout) {
|
||||
$layout = 'default';
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->startTimer('flex-object-' . ($debugKey = uniqid($this->getType(false), false)), 'Render Object ' . $this->getType(false));
|
||||
|
||||
$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->flexDirectory->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);
|
||||
|
||||
$grav->fireEvent('onFlexObjectRender', new Event([
|
||||
'object' => $this,
|
||||
'layout' => &$layout,
|
||||
'context' => &$context
|
||||
]));
|
||||
|
||||
$output = $this->getTemplate($layout)->render(
|
||||
['grav' => $grav, 'block' => $block, 'object' => $this, 'layout' => $layout] + $context
|
||||
);
|
||||
|
||||
$block->setContent($output);
|
||||
|
||||
try {
|
||||
$cache && $cache->set($key, $block->toArray());
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-object-' . $debugKey);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form field compatibility.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function value($name, $default = null)
|
||||
{
|
||||
if ($name === 'storage_key') {
|
||||
return $this->getStorageKey();
|
||||
}
|
||||
|
||||
return $this->getNestedProperty($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function exists()
|
||||
{
|
||||
$key = $this->getStorageKey();
|
||||
|
||||
return $key && $this->getFlexDirectory()->getStorage()->hasKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->getElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function prepareStorage()
|
||||
{
|
||||
return $this->getElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStorageFolder()
|
||||
{
|
||||
return $this->getFlexDirectory()->getStorageFolder($this->getStorageKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaFolder()
|
||||
{
|
||||
return $this->getFlexDirectory()->getMediaFolder($this->getStorageKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function triggerEvent($name)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new object into storage.
|
||||
*
|
||||
* @param string|null $key Optional new key.
|
||||
* @return $this
|
||||
*/
|
||||
public function create($key = null)
|
||||
{
|
||||
if ($key) {
|
||||
$this->setStorageKey($key);
|
||||
}
|
||||
|
||||
if ($this->exists()) {
|
||||
throw new \RuntimeException('Cannot create new object (Already exists)');
|
||||
}
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
|
||||
|
||||
try {
|
||||
$this->getFlexDirectory()->clearCache();
|
||||
if (method_exists($this, 'clearMediaCache')) {
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
// Caching failed, but we can ignore that for now.
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
|
||||
|
||||
try {
|
||||
$this->getFlexDirectory()->clearCache();
|
||||
if (method_exists($this, 'clearMediaCache')) {
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
// Caching failed, but we can ignore that for now.
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize()
|
||||
{
|
||||
return $this->jsonSerialize() + ['storage_key' => $this->getStorageKey(), 'storage_timestamp' => $this->getTimestamp()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
*/
|
||||
protected function doUnserialize(array $serialized)
|
||||
{
|
||||
$type = $serialized['type'] ?? 'unknown';
|
||||
|
||||
if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) {
|
||||
$type = $serialized['type'] ?? 'unknown';
|
||||
throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data");
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
/** @var Flex $flex */
|
||||
$flex = $grav['flex_directory'];
|
||||
$directory = $flex->getDirectory($type);
|
||||
if (!$directory) {
|
||||
throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found");
|
||||
}
|
||||
$this->flexDirectory = $directory;
|
||||
$this->storageKey = $serialized['storage_key'];
|
||||
$this->timestamp = $serialized['storage_timestamp'];
|
||||
|
||||
$this->setKey($serialized['key']);
|
||||
$this->setElements($serialized['elements']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Medium|null
|
||||
*/
|
||||
protected function createMedium($uri)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$file = $uri ? $locator->findResource($uri) : null;
|
||||
|
||||
return $file ? MediumFactory::fromFile($file) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $property
|
||||
* @return FlexCollection
|
||||
*/
|
||||
protected function getCollectionByProperty($type, $property)
|
||||
{
|
||||
$directory = $this->getRelatedDirectory($type);
|
||||
$collection = $directory->getCollection();
|
||||
$list = $this->getNestedProperty($property) ?: [];
|
||||
|
||||
$collection = $collection->filter(function ($object) use ($list) { return \in_array($object->id, $list, true); });
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return FlexDirectory
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function getRelatedDirectory($type)
|
||||
{
|
||||
/** @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 \Twig_Template
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Syntax
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
try {
|
||||
return $twig->twig()->resolveTemplate(["flex-objects/layouts/{$this->getType(false)}/object/{$layout}.html.twig"]);
|
||||
} catch (\Twig_Error_Loader $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
return $twig->twig()->resolveTemplate(["flex-objects/layouts/404.html.twig"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
*/
|
||||
protected function filterElements(array &$elements)
|
||||
{
|
||||
if (!empty($elements['storage_key'])) {
|
||||
$this->storageKey = trim($elements['storage_key']);
|
||||
}
|
||||
if (!empty($elements['storage_timestamp'])) {
|
||||
$this->timestamp = (int)$elements['storage_timestamp'];
|
||||
}
|
||||
|
||||
unset ($elements['storage_key'], $elements['storage_timestamp']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Interface FlexAuthorizeInterface
|
||||
* @package Grav\Framework\User\Interfaces
|
||||
*/
|
||||
interface FlexAuthorizeInterface
|
||||
{
|
||||
/**
|
||||
* @param string $action One of: create, read, update, delete, save, list
|
||||
* @param string|null $scope One of: admin, site
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null) : bool;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
|
||||
/**
|
||||
* Interface FlexCollectionInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexCollectionInterface extends ObjectCollectionInterface, NestedObjectInterface
|
||||
{
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param FlexDirectory $type
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $elements, FlexDirectory $type);
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getFlexDirectory() : FlexDirectory;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
|
||||
/**
|
||||
* Interface FlexObjectInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexObjectInterface extends NestedObjectInterface, \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param string $key
|
||||
* @param FlexDirectory $type
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $elements, $key, FlexDirectory $type);
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getFlexDirectory() : FlexDirectory;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimestamp() : int;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Interface FlexStorageInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexStorageInterface
|
||||
{
|
||||
/**
|
||||
* StorageInterface constructor.
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options);
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExistingKeys() : array;
|
||||
|
||||
/**
|
||||
* Check if storage has a row for the key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasKey(string $key) : bool;
|
||||
|
||||
/**
|
||||
* Create new rows. New keys will be assigned when the objects are created.
|
||||
*
|
||||
* @param array $rows Array of rows.
|
||||
* @return array Returns created rows. Note that existing rows will fail to save and have null value.
|
||||
*/
|
||||
public function createRows(array $rows) : array;
|
||||
|
||||
/**
|
||||
* Read rows. 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 variable for storing only fetched items.
|
||||
* @return array Returns rows. Note that non-existing rows have null value.
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null) : array;
|
||||
|
||||
/**
|
||||
* Update existing rows.
|
||||
*
|
||||
* @param array $rows Array of [key => row] pairs.
|
||||
* @return array Returns updated rows. Note that non-existing rows will fail to save and have null value.
|
||||
*/
|
||||
public function updateRows(array $rows) : array;
|
||||
|
||||
/**
|
||||
* Delete rows.
|
||||
*
|
||||
* @param array $rows Array of [key => row] pairs.
|
||||
* @return array Returns deleted rows. Note that non-existing rows have null value.
|
||||
*/
|
||||
public function deleteRows(array $rows) : array;
|
||||
|
||||
/**
|
||||
* Replace rows regardless if they exist or not.
|
||||
*
|
||||
* All rows should have a specified key for this to work.
|
||||
*
|
||||
* @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
|
||||
* @return string
|
||||
*/
|
||||
public function getStoragePath(string $key = null) : string;
|
||||
|
||||
/**
|
||||
* Get filesystem path for the collection or object media.
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaPath(string $key = null) : string;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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\Common\Helpers\Base32;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\File\Formatter\FormatterInterface;
|
||||
use Grav\Framework\File\Formatter\JsonFormatter;
|
||||
use Grav\Framework\File\Formatter\MarkdownFormatter;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
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 FormatterInterface */
|
||||
protected $dataFormatter;
|
||||
|
||||
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) : File
|
||||
{
|
||||
$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 Base32::encode(Utils::generateRandomString(10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key is valid.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateKey(string $key) : bool
|
||||
{
|
||||
return (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key);
|
||||
}
|
||||
}
|
||||
75
system/src/Grav/Framework/Flex/Storage/FileStorage.php
Normal file
75
system/src/Grav/Framework/Flex/Storage/FileStorage.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Storage;
|
||||
|
||||
/**
|
||||
* Class FileStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class FileStorage extends FolderStorage
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->dataPattern = '%1s/%2s';
|
||||
|
||||
if (!isset($options['formatter']) && isset($options['pattern'])) {
|
||||
$options['formatter'] = $this->detectDataFormatter($options['pattern']);
|
||||
}
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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 findAllKeys() : 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))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$key] = $info->getMTime();
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
371
system/src/Grav/Framework/Flex/Storage/FolderStorage.php
Normal file
371
system/src/Grav/Framework/Flex/Storage/FolderStorage.php
Normal file
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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 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 = '%1s/%2s/item';
|
||||
|
||||
/**
|
||||
* {@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}
|
||||
*/
|
||||
public function getExistingKeys() : array
|
||||
{
|
||||
return $this->findAllKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasKey(string $key) : bool
|
||||
{
|
||||
return $key && file_exists($this->getPathFromKey($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
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.
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->hasKey($key) ? $this->loadFile($file) : null;
|
||||
if (null !== $fetched) {
|
||||
$fetched[$key] = $list[$key];
|
||||
}
|
||||
} else {
|
||||
// Keep the row if it has been loaded.
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->hasKey($key) ? $this->saveFile($file, $row) : null;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->hasKey($key) ? $this->deleteFile($file) : null;
|
||||
|
||||
$storage = $this->getStoragePath($key);
|
||||
$media = $this->getMediaPath($key);
|
||||
|
||||
$this->deleteFolder($storage, true);
|
||||
$media && $this->deleteFolder($media, true);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function replaceRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->saveFile($file, $row);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
public function getStoragePath(string $key = null) : string
|
||||
{
|
||||
if (null === $key) {
|
||||
$path = $this->dataFolder;
|
||||
} else {
|
||||
$path = sprintf($this->dataPattern, $this->dataFolder, $key);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File $file
|
||||
* @return array|null
|
||||
*/
|
||||
protected function loadFile(File $file) : ?array
|
||||
{
|
||||
return $file->exists() ? (array)$file->content() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 findAllKeys() : 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->isDir() || !($key = $this->getKeyFromPath($filename))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$key] = $info->getMTime();
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
|
||||
|
||||
$this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension;
|
||||
$this->dataFolder = $options['folder'];
|
||||
}
|
||||
}
|
||||
258
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
Normal file
258
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
public function getExistingKeys() : array
|
||||
{
|
||||
return $this->findAllKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasKey(string $key) : bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = $this->getNewKey();
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
|
||||
$list && $this->save();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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.
|
||||
$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}
|
||||
*/
|
||||
public function updateRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if ($this->hasKey($key)) {
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$list && $this->save();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if ($this->hasKey($key)) {
|
||||
unset($this->data[$key]);
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$list && $this->save();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function replaceRows(array $rows) : array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$this->data[$key] = $list[$key] = $row;
|
||||
}
|
||||
|
||||
$list && $this->save();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// Change single key in the array without changing the order or value.
|
||||
$keys = array_keys($this->data);
|
||||
$keys[array_search($src, $keys, true)] = $dst;
|
||||
|
||||
$this->data = array_combine($keys, $this->data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStoragePath(string $key = null) : string
|
||||
{
|
||||
return $this->dataFolder . '/' . $this->dataPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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
|
||||
{
|
||||
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 findAllKeys() : array
|
||||
{
|
||||
$file = $this->getFile($this->getStoragePath());
|
||||
$modified = $file->modified();
|
||||
|
||||
$this->data = (array) $file->content();
|
||||
|
||||
$list = [];
|
||||
foreach ($this->data as $key => $info) {
|
||||
$list[$key] = $modified;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey() : string
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->findAllKeys();
|
||||
}
|
||||
|
||||
// Make sure that the key doesn't exist.
|
||||
do {
|
||||
$key = $this->generateKey();
|
||||
} while (isset($this->data[$key]));
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
44
system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php
Normal file
44
system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 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\User;
|
||||
|
||||
/**
|
||||
* Implements basic ACL
|
||||
*/
|
||||
trait FlexAuthorizeTrait
|
||||
{
|
||||
private $authorize = '%s.flex-object.%s';
|
||||
|
||||
public function authorize(string $action, string $scope = null) : bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var User $user */
|
||||
$user = Grav::instance()['user'];
|
||||
|
||||
$scope = $scope ?? isset($grav['admin']) ? 'admin' : 'site';
|
||||
|
||||
if ($action === 'save') {
|
||||
$action = $this->exists() ? 'update' : 'create';
|
||||
}
|
||||
|
||||
return $user->authorize(sprintf($this->authorize, $scope, $action)) || $user->authorize('admin.super');
|
||||
}
|
||||
|
||||
protected function setAuthorizeRule(string $authorize) : void
|
||||
{
|
||||
$this->authorize = $authorize;
|
||||
}
|
||||
}
|
||||
135
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
Normal file
135
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Framework\Flex\Traits;
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Traits\MediaTrait;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
|
||||
/**
|
||||
* Implements Grav Page content and header manipulation methods.
|
||||
*/
|
||||
trait FlexMediaTrait
|
||||
{
|
||||
use MediaTrait;
|
||||
|
||||
public function uploadMediaFile(UploadedFileInterface $uploadedFile) : void
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$language = $grav['language'];
|
||||
|
||||
switch ($uploadedFile->getError()) {
|
||||
case UPLOAD_ERR_OK:
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
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);
|
||||
}
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
$grav_limit = (int) $config->get('system.media.upload_limit', 0);
|
||||
|
||||
if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
|
||||
}
|
||||
|
||||
// Check the file extension.
|
||||
$filename = $uploadedFile->getClientFilename();
|
||||
$fileParts = pathinfo($filename);
|
||||
$extension = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : '';
|
||||
|
||||
// If not a supported type, return
|
||||
if (!$extension || !$config->get("media.types.{$extension}")) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
|
||||
}
|
||||
|
||||
$media = $this->getMedia();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
$path = $media->path();
|
||||
if ($locator->isStream($path)) {
|
||||
$path = $locator->findResource($path, true, true);
|
||||
$locator->clearCache($path);
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload it
|
||||
$uploadedFile->moveTo(sprintf('%s/%s', $path, $filename));
|
||||
} catch (\Exception $e) {
|
||||
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'];
|
||||
|
||||
$media = $this->getMedia();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$targetPath = $media->path() . '/' . $filename;
|
||||
if ($locator->isStream($targetPath)) {
|
||||
$targetPath = $locator->findResource($targetPath, true, true);
|
||||
$locator->clearCache($targetPath);
|
||||
}
|
||||
|
||||
$fileParts = pathinfo($filename);
|
||||
$found = false;
|
||||
|
||||
if (file_exists($targetPath)) {
|
||||
$found = true;
|
||||
|
||||
$result = unlink($targetPath);
|
||||
if (!$result) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Extra Files
|
||||
foreach (scandir($media->path(), SCANDIR_SORT_NONE) as $file) {
|
||||
if (preg_match("/{$fileParts['filename']}@\d+x\.{$fileParts['extension']}(?:\.meta\.yaml)?$|{$filename}\.meta\.yaml$/", $file)) {
|
||||
|
||||
$targetPath = $media->path() . '/' . $file;
|
||||
if ($locator->isStream($targetPath)) {
|
||||
$targetPath = $locator->findResource($targetPath, true, true);
|
||||
$locator->clearCache($targetPath);
|
||||
}
|
||||
|
||||
$result = unlink($targetPath);
|
||||
if (!$result) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
|
||||
}
|
||||
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearMediaCache();
|
||||
|
||||
if (!$found) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . $filename, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user