Added Grav\Framework\Flex classes

This commit is contained in:
Matias Griese
2018-09-12 15:52:28 +03:00
parent e5c6788243
commit 593abccedc
16 changed files with 3361 additions and 0 deletions

View File

@@ -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

View 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;
}
}

View 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;
}
}

View 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')
);
}
}
}

View 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();
}
}

View 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']);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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;
}
}

View 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'];
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}