This commit is contained in:
2019-09-27 00:29:18 +02:00
parent 40af43681e
commit b857814072
470 changed files with 37562 additions and 10060 deletions
@@ -1,14 +1,15 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
use Psr\SimpleCache\InvalidArgumentException;
/**
* Cache trait for PSR-16 compatible "Simple Cache" implementation
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -20,7 +21,7 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
class ChainCache extends AbstractCache
{
/**
* @var array|CacheInterface[]
* @var CacheInterface[]
*/
protected $caches;
@@ -33,7 +34,7 @@ class ChainCache extends AbstractCache
* Chain Cache constructor.
* @param array $caches
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function __construct(array $caches, $defaultLifetime = null)
{
@@ -48,7 +49,7 @@ class ChainCache extends AbstractCache
throw new InvalidArgumentException(
sprintf(
"The class '%s' does not implement the '%s' interface",
get_class($cache),
\get_class($cache),
CacheInterface::class
)
);
@@ -56,7 +57,7 @@ class ChainCache extends AbstractCache
}
$this->caches = array_values($caches);
$this->count = count($caches);
$this->count = \count($caches);
}
/**
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -29,7 +30,7 @@ class DoctrineCache extends AbstractCache
* @param CacheProvider $doctrineCache
* @param string $namespace
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function __construct(CacheProvider $doctrineCache, $namespace = '', $defaultLifetime = null)
{
@@ -38,7 +39,9 @@ class DoctrineCache extends AbstractCache
// Set namespace to Doctrine Cache provider if it was given.
$namespace = $this->getNamespace();
$namespace && $doctrineCache->setNamespace($namespace);
if ($namespace) {
$doctrineCache->setNamespace($namespace);
}
$this->driver = $doctrineCache;
}
@@ -96,20 +99,10 @@ class DoctrineCache extends AbstractCache
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function doDeleteMultiple($keys)
{
// TODO: Remove when Doctrine Cache has been updated to support the feature.
if (!method_exists($this->driver, 'deleteMultiple')) {
$success = true;
foreach ($keys as $key) {
$success = $this->delete($key) && $success;
}
return $success;
}
return $this->driver->deleteMultiple($keys);
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -27,9 +28,11 @@ class FileCache extends AbstractCache
/**
* @inheritdoc
*/
public function __construct($namespace = '', $defaultLifetime = null)
public function __construct($namespace = '', $defaultLifetime = null, $folder = null)
{
parent::__construct($namespace, $defaultLifetime ?: 31557600); // = 1 year
$this->initFileCache($namespace, $folder ?? '');
}
/**
@@ -62,7 +65,7 @@ class FileCache extends AbstractCache
/**
* @inheritdoc
* @throws \Psr\SimpleCache\CacheException
* @throws \Psr\SimpleCache\CacheException|InvalidArgumentException
*/
public function doSet($key, $value, $ttl)
{
@@ -126,8 +129,8 @@ class FileCache extends AbstractCache
$hash = str_replace('/', '-', base64_encode(hash('sha256', static::class . $key, true)));
$dir = $this->directory . $hash[0] . DIRECTORY_SEPARATOR . $hash[1] . DIRECTORY_SEPARATOR;
if ($mkdir && !file_exists($dir)) {
@mkdir($dir, 0777, true);
if ($mkdir) {
$this->mkdir($dir);
}
return $dir . substr($hash, 2, 20);
@@ -136,9 +139,9 @@ class FileCache extends AbstractCache
/**
* @param string $namespace
* @param string $directory
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
private function init($namespace, $directory)
protected function initFileCache($namespace, $directory)
{
if (!isset($directory[0])) {
$directory = sys_get_temp_dir() . '/grav-cache';
@@ -153,9 +156,7 @@ class FileCache extends AbstractCache
$directory .= DIRECTORY_SEPARATOR . $namespace;
}
if (!file_exists($directory)) {
@mkdir($directory, 0777, true);
}
$this->mkdir($directory);
$directory .= DIRECTORY_SEPARATOR;
// On Windows the whole path is limited to 258 chars
@@ -192,6 +193,28 @@ class FileCache extends AbstractCache
}
}
/**
* @param string $dir
* @throws \RuntimeException
*/
private function mkdir($dir)
{
// Silence error for open_basedir; should fail in mkdir instead.
if (@is_dir($dir)) {
return;
}
$success = @mkdir($dir, 0777, true);
if (!$success) {
// Take yet another look, make sure that the folder doesn't exist.
clearstatcache(true, $dir);
if (!@is_dir($dir)) {
throw new \RuntimeException(sprintf('Unable to create directory: %s', $dir));
}
}
}
/**
* @internal
* @throws \ErrorException
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -17,8 +18,8 @@ use Grav\Framework\Cache\AbstractCache;
*/
class SessionCache extends AbstractCache
{
const VALUE = 0;
const LIFETIME = 1;
public const VALUE = 0;
public const LIFETIME = 1;
public function doGet($key, $miss)
{
@@ -66,7 +67,7 @@ class SessionCache extends AbstractCache
protected function doGetStored($key)
{
$stored = isset($_SESSION[$this->getNamespace()][$key]) ? $_SESSION[$this->getNamespace()][$key] : null;
$stored = $_SESSION[$this->getNamespace()][$key] ?? null;
if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) {
unset($_SESSION[$this->getNamespace()][$key]);
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
+27 -26
View File
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -33,7 +34,7 @@ trait CacheTrait
*
* @param string $namespace
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
protected function init($namespace = '', $defaultLifetime = null)
{
@@ -43,7 +44,7 @@ trait CacheTrait
}
/**
* @param $validation
* @param bool $validation
*/
public function setValidation($validation)
{
@@ -68,7 +69,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function get($key, $default = null)
{
@@ -81,7 +82,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function set($key, $value, $ttl = null)
{
@@ -95,7 +96,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function delete($key)
{
@@ -114,17 +115,17 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function getMultiple($keys, $default = null)
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} elseif (!is_array($keys)) {
} elseif (!\is_array($keys)) {
throw new InvalidArgumentException(
sprintf(
'Cache keys must be array or Traversable, "%s" given',
is_object($keys) ? get_class($keys) : gettype($keys)
\is_object($keys) ? \get_class($keys) : \gettype($keys)
)
);
}
@@ -154,7 +155,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function setMultiple($values, $ttl = null)
{
@@ -164,7 +165,7 @@ trait CacheTrait
throw new InvalidArgumentException(
sprintf(
'Cache values must be array or Traversable, "%s" given',
is_object($values) ? get_class($values) : gettype($values)
\is_object($values) ? \get_class($values) : \gettype($values)
)
);
}
@@ -185,7 +186,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function deleteMultiple($keys)
{
@@ -195,7 +196,7 @@ trait CacheTrait
throw new InvalidArgumentException(
sprintf(
'Cache keys must be array or Traversable, "%s" given',
is_object($keys) ? get_class($keys) : gettype($keys)
\is_object($keys) ? \get_class($keys) : \gettype($keys)
)
);
}
@@ -211,7 +212,7 @@ trait CacheTrait
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
public function has($key)
{
@@ -278,25 +279,25 @@ trait CacheTrait
abstract public function doHas($key);
/**
* @param string $key
* @throws \Psr\SimpleCache\InvalidArgumentException
* @param string|mixed $key
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
protected function validateKey($key)
{
if (!is_string($key)) {
if (!\is_string($key)) {
throw new InvalidArgumentException(
sprintf(
'Cache key must be string, "%s" given',
is_object($key) ? get_class($key) : gettype($key)
\is_object($key) ? \get_class($key) : \gettype($key)
)
);
}
if (!isset($key[0])) {
throw new InvalidArgumentException('Cache key length must be greater than zero');
}
if (strlen($key) > 64) {
if (\strlen($key) > 64) {
throw new InvalidArgumentException(
sprintf('Cache key length must be less than 65 characters, key had %s characters', strlen($key))
sprintf('Cache key length must be less than 65 characters, key had %s characters', \strlen($key))
);
}
if (strpbrk($key, '{}()/\@:') !== false) {
@@ -308,7 +309,7 @@ trait CacheTrait
/**
* @param array $keys
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
protected function validateKeys($keys)
{
@@ -322,9 +323,9 @@ trait CacheTrait
}
/**
* @param null|int|\DateInterval $ttl
* @param null|int|\DateInterval|mixed $ttl
* @return int|null
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
protected function convertTtl($ttl)
{
@@ -332,18 +333,18 @@ trait CacheTrait
return $this->getDefaultLifetime();
}
if (is_int($ttl)) {
if (\is_int($ttl)) {
return $ttl;
}
if ($ttl instanceof \DateInterval) {
$ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U');
$ttl = (int) \DateTime::createFromFormat('U', '0')->add($ttl)->format('U');
}
throw new InvalidArgumentException(
sprintf(
'Expiration date must be an integer, a DateInterval or null, "%s" given',
is_object($ttl) ? get_class($ttl) : gettype($ttl)
\is_object($ttl) ? \get_class($ttl) : \gettype($ttl)
)
);
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -37,7 +38,7 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
protected $createObjectFunction;
/**
* @var callable
* @var callable|null
*/
protected $filterFunction;
@@ -94,7 +95,7 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
if ($orderings = $criteria->getOrderings()) {
$next = null;
foreach (array_reverse($orderings) as $field => $ordering) {
$next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
$next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next);
}
uasort($filtered, $next);
@@ -106,7 +107,7 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
$length = $criteria->getMaxResults();
if ($offset || $length) {
$filtered = array_slice($filtered, (int)$offset, $length);
$filtered = \array_slice($filtered, (int)$offset, $length);
}
return new ArrayCollection($filtered);
@@ -0,0 +1,510 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use ArrayIterator;
use Closure;
/**
* Abstract Index Collection.
*/
abstract class AbstractIndexCollection implements CollectionInterface
{
/** @var array */
private $entries;
/**
* Initializes a new IndexCollection.
*
* @param array $entries
*/
public function __construct(array $entries = [])
{
$this->entries = $entries;
}
/**
* {@inheritDoc}
*/
public function toArray()
{
return $this->loadElements($this->entries);
}
/**
* {@inheritDoc}
*/
public function first()
{
$value = reset($this->entries);
$key = key($this->entries);
return $this->loadElement($key, $value);
}
/**
* {@inheritDoc}
*/
public function last()
{
$value = end($this->entries);
$key = key($this->entries);
return $this->loadElement($key, $value);
}
/**
* {@inheritDoc}
*/
public function key()
{
return key($this->entries);
}
/**
* {@inheritDoc}
*/
public function next()
{
$value = next($this->entries);
$key = key($this->entries);
return $this->loadElement($key, $value);
}
/**
* {@inheritDoc}
*/
public function current()
{
$value = current($this->entries);
$key = key($this->entries);
return $this->loadElement($key, $value);
}
/**
* {@inheritDoc}
*/
public function remove($key)
{
if (!array_key_exists($key, $this->entries)) {
return null;
}
$value = $this->entries[$key];
unset($this->entries[$key]);
return $this->loadElement($key, $value);
}
/**
* {@inheritDoc}
*/
public function removeElement($element)
{
$key = $this->isAllowedElement($element) ? $element->getKey() : null;
if (!$key || !isset($this->entries[$key])) {
return false;
}
unset($this->entries[$key]);
return true;
}
/**
* Required by interface ArrayAccess.
*
* {@inheritDoc}
*/
public function offsetExists($offset)
{
return $this->containsKey($offset);
}
/**
* Required by interface ArrayAccess.
*
* {@inheritDoc}
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Required by interface ArrayAccess.
*
* {@inheritDoc}
*/
public function offsetSet($offset, $value)
{
if (null === $offset) {
$this->add($value);
}
$this->set($offset, $value);
}
/**
* Required by interface ArrayAccess.
*
* {@inheritDoc}
*/
public function offsetUnset($offset)
{
return $this->remove($offset);
}
/**
* {@inheritDoc}
*/
public function containsKey($key)
{
return isset($this->entries[$key]) || array_key_exists($key, $this->entries);
}
/**
* {@inheritDoc}
*/
public function contains($element)
{
$key = $this->isAllowedElement($element) ? $element->getKey() : null;
return $key && isset($this->entries[$key]);
}
/**
* {@inheritDoc}
*/
public function exists(Closure $p)
{
return $this->loadCollection($this->entries)->exists($p);
}
/**
* {@inheritDoc}
*/
public function indexOf($element)
{
$key = $this->isAllowedElement($element) ? $element->getKey() : null;
return $key && isset($this->entries[$key]) ? $key : null;
}
/**
* {@inheritDoc}
*/
public function get($key)
{
if (!isset($this->entries[$key])) {
return null;
}
return $this->loadElement($key, $this->entries[$key]);
}
/**
* {@inheritDoc}
*/
public function getKeys()
{
return array_keys($this->entries);
}
/**
* {@inheritDoc}
*/
public function getValues()
{
return array_values($this->loadElements($this->entries));
}
/**
* {@inheritDoc}
*/
public function count()
{
return \count($this->entries);
}
/**
* {@inheritDoc}
*/
public function set($key, $value)
{
if (!$this->isAllowedElement($value)) {
throw new \InvalidArgumentException('Invalid argument $value');
}
if ($key !== $value->getKey()) {
$value->setKey($key);
}
$this->entries[$key] = $this->getElementMeta($value);
}
/**
* {@inheritDoc}
*/
public function add($element)
{
if (!$this->isAllowedElement($element)) {
throw new \InvalidArgumentException('Invalid argument $element');
}
$this->entries[$element->getKey()] = $this->getElementMeta($element);
return true;
}
/**
* {@inheritDoc}
*/
public function isEmpty()
{
return empty($this->entries);
}
/**
* Required by interface IteratorAggregate.
*
* {@inheritDoc}
*/
public function getIterator()
{
return new ArrayIterator($this->loadElements());
}
/**
* {@inheritDoc}
*/
public function map(Closure $func)
{
return $this->loadCollection($this->entries)->map($func);
}
/**
* {@inheritDoc}
*/
public function filter(Closure $p)
{
return $this->loadCollection($this->entries)->filter($p);
}
/**
* {@inheritDoc}
*/
public function forAll(Closure $p)
{
return $this->loadCollection($this->entries)->forAll($p);
}
/**
* {@inheritDoc}
*/
public function partition(Closure $p)
{
return $this->loadCollection($this->entries)->partition($p);
}
/**
* Returns a string representation of this object.
*
* @return string
*/
public function __toString()
{
return __CLASS__ . '@' . spl_object_hash($this);
}
/**
* {@inheritDoc}
*/
public function clear()
{
$this->entries = [];
}
/**
* {@inheritDoc}
*/
public function slice($offset, $length = null)
{
return $this->loadElements(\array_slice($this->entries, $offset, $length, true));
}
/**
* @param int $start
* @param int|null $limit
* @return static
*/
public function limit($start, $limit = null)
{
return $this->createFrom(\array_slice($this->entries, $start, $limit, true));
}
/**
* Reverse the order of the items.
*
* @return static
*/
public function reverse()
{
return $this->createFrom(array_reverse($this->entries));
}
/**
* Shuffle items.
*
* @return static
*/
public function shuffle()
{
$keys = $this->getKeys();
shuffle($keys);
return $this->createFrom(array_replace(array_flip($keys), $this->entries));
}
/**
* Select items from collection.
*
* Collection is returned in the order of $keys given to the function.
*
* @param array $keys
* @return static
*/
public function select(array $keys)
{
$list = [];
foreach ($keys as $key) {
if (isset($this->entries[$key])) {
$list[$key] = $this->entries[$key];
}
}
return $this->createFrom($list);
}
/**
* Un-select items from collection.
*
* @param array $keys
* @return static
*/
public function unselect(array $keys)
{
return $this->select(array_diff($this->getKeys(), $keys));
}
/**
* Split collection into chunks.
*
* @param int $size Size of each chunk.
* @return array
*/
public function chunk($size)
{
return $this->loadCollection($this->entries)->chunk($size);
}
/**
* @return string
*/
public function serialize()
{
return serialize(['entries' => $this->entries]);
}
/**
* @param string $serialized
*/
public function unserialize($serialized)
{
$data = unserialize($serialized, ['allowed_classes' => false]);
$this->entries = $data['entries'];
}
/**
* Implements JsonSerializable interface.
*
* @return array
*/
public function jsonSerialize()
{
return $this->loadCollection()->jsonSerialize();
}
/**
* 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 $entries Elements.
*
* @return static
*/
protected function createFrom(array $entries)
{
return new static($entries);
}
/**
* @return array
*/
protected function getEntries() : array
{
return $this->entries;
}
/**
* @param array $entries
*/
protected function setEntries(array $entries) : void
{
$this->entries = $entries;
}
/**
* @param string $key
* @param mixed $value
* @return mixed|null
*/
abstract protected function loadElement($key, $value);
/**
* @param array|null $entries
* @return array
*/
abstract protected function loadElements(array $entries = null) : array;
/**
* @param array|null $entries
* @return CollectionInterface
*/
abstract protected function loadCollection(array $entries = null) : CollectionInterface;
/**
* @param mixed $value
* @return bool
*/
abstract protected function isAllowedElement($value) : bool;
/**
* @param mixed $element
* @return mixed
*/
abstract protected function getElementMeta($element);
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -51,6 +52,24 @@ abstract class AbstractLazyCollection extends BaseAbstractLazyCollection impleme
return $this->collection->chunk($size);
}
/**
* {@inheritDoc}
*/
public function select(array $keys)
{
$this->initialize();
return $this->collection->select($keys);
}
/**
* {@inheritDoc}
*/
public function unselect(array $keys)
{
$this->initialize();
return $this->collection->unselect($keys);
}
/**
* {@inheritDoc}
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -52,7 +53,38 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
}
/**
* Implementes JsonSerializable interface.
* Select items from collection.
*
* Collection is returned in the order of $keys given to the function.
*
* @param array $keys
* @return static
*/
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.
*
* @param array $keys
* @return static
*/
public function unselect(array $keys)
{
return $this->select(array_diff($this->getKeys(), $keys));
}
/**
* Implements JsonSerializable interface.
*
* @return array
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -38,4 +39,22 @@ interface CollectionInterface extends Collection, \JsonSerializable
* @return array
*/
public function chunk($size);
/**
* Select items from collection.
*
* Collection is returned in the order of $keys given to the function.
*
* @param array $keys
* @return static
*/
public function select(array $keys);
/**
* Un-select items from collection.
*
* @param array $keys
* @return static
*/
public function unselect(array $keys);
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -17,9 +18,9 @@ use Doctrine\Common\Collections\Selectable;
*/
interface FileCollectionInterface extends CollectionInterface, Selectable
{
const INCLUDE_FILES = 1;
const INCLUDE_FOLDERS = 2;
const RECURSIVE = 4;
public const INCLUDE_FILES = 1;
public const INCLUDE_FOLDERS = 2;
public const RECURSIVE = 4;
/**
* @return string
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -28,6 +29,7 @@ class ContentBlock implements ContentBlockInterface
protected $content = '';
protected $blocks = [];
protected $checksum;
protected $cached = true;
/**
* @param string $id
@@ -46,10 +48,10 @@ class ContentBlock implements ContentBlockInterface
public static function fromArray(array $serialized)
{
try {
$type = isset($serialized['_type']) ? $serialized['_type'] : null;
$id = isset($serialized['id']) ? $serialized['id'] : null;
$type = $serialized['_type'] ?? null;
$id = $serialized['id'] ?? null;
if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) {
if (!$type || !$id || !is_a($type, ContentBlockInterface::class, true)) {
throw new \InvalidArgumentException('Bad data');
}
@@ -104,9 +106,10 @@ class ContentBlock implements ContentBlockInterface
}
$array = [
'_type' => get_class($this),
'_type' => \get_class($this),
'_version' => $this->version,
'id' => $this->id
'id' => $this->id,
'cached' => $this->cached
];
if ($this->checksum) {
@@ -163,8 +166,9 @@ class ContentBlock implements ContentBlockInterface
{
$this->checkVersion($serialized);
$this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
$this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null;
$this->id = $serialized['id'] ?? $this->generateId();
$this->checksum = $serialized['checksum'] ?? null;
$this->cached = $serialized['cached'] ?? null;
if (isset($serialized['content'])) {
$this->setContent($serialized['content']);
@@ -176,6 +180,34 @@ class ContentBlock implements ContentBlockInterface
}
}
/**
* @return bool
*/
public function isCached()
{
if (!$this->cached) {
return false;
}
foreach ($this->blocks as $block) {
if (!$block->isCached()) {
return false;
}
}
return true;
}
/**
* @return $this
*/
public function disableCache()
{
$this->cached = false;
return $this;
}
/**
* @param string $checksum
* @return $this
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -131,7 +132,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
*/
public function addStyle($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
if (!\is_array($element)) {
$element = ['href' => (string) $element];
}
if (empty($element['href'])) {
@@ -175,7 +176,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
*/
public function addInlineStyle($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
if (!\is_array($element)) {
$element = ['content' => (string) $element];
}
if (empty($element['content'])) {
@@ -206,7 +207,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
*/
public function addScript($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
if (!\is_array($element)) {
$element = ['src' => (string) $element];
}
if (empty($element['src'])) {
@@ -243,7 +244,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
*/
public function addInlineScript($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
if (!\is_array($element)) {
$element = ['content' => (string) $element];
}
if (empty($element['content'])) {
@@ -274,7 +275,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
*/
public function addHtml($html, $priority = 0, $location = 'bottom')
{
if (empty($html) || !is_string($html)) {
if (empty($html) || !\is_string($html)) {
return false;
}
if (!isset($this->html[$location])) {
@@ -302,7 +303,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
];
foreach ($this->blocks as $block) {
if ($block instanceof HtmlBlock) {
if ($block instanceof self) {
$blockAssets = $block->getAssetsFast();
$assets['frameworks'] += $blockAssets['frameworks'];
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -0,0 +1,27 @@
<?php
/**
* @package Grav\Framework\DI
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
declare(strict_types=1);
namespace Grav\Framework\DI;
use Psr\Container\ContainerInterface;
class Container extends \Pimple\Container implements ContainerInterface
{
public function get($id)
{
return $this->offsetGet($id);
}
public function has($id): bool
{
return $this->offsetExists($id);
}
}
@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Interfaces\FileInterface;
use Grav\Framework\Filesystem\Filesystem;
class AbstractFile implements FileInterface
{
/** @var Filesystem */
private $filesystem;
/** @var string */
private $filepath;
/** @var string|null */
private $filename;
/** @var string|null */
private $path;
/** @var string|null */
private $basename;
/** @var string|null */
private $extension;
/** @var resource|null */
private $handle;
/** @var bool */
private $locked = false;
/**
* @param string $filepath
* @param Filesystem|null $filesystem
*/
public function __construct(string $filepath, Filesystem $filesystem = null)
{
$this->filesystem = $filesystem ?? Filesystem::getInstance();
$this->setFilepath($filepath);
}
/**
* Unlock file when the object gets destroyed.
*/
public function __destruct()
{
if ($this->isLocked()) {
$this->unlock();
}
}
public function __clone()
{
$this->handle = null;
$this->locked = false;
}
/**
* @return string
*/
public function serialize(): string
{
return serialize($this->doSerialize());
}
/**
* @param string $serialized
*/
public function unserialize($serialized): void
{
$this->doUnserialize(unserialize($serialized, ['allowed_classes' => false]));
}
/**
* {@inheritdoc}
* @see FileInterface::getFilePath()
*/
public function getFilePath(): string
{
return $this->filepath;
}
/**
* {@inheritdoc}
* @see FileInterface::getPath()
*/
public function getPath(): string
{
if (null === $this->path) {
$this->setPathInfo();
}
return $this->path;
}
/**
* {@inheritdoc}
* @see FileInterface::getFilename()
*/
public function getFilename(): string
{
if (null === $this->filename) {
$this->setPathInfo();
}
return $this->filename;
}
/**
* {@inheritdoc}
* @see FileInterface::getBasename()
*/
public function getBasename(): string
{
if (null === $this->basename) {
$this->setPathInfo();
}
return $this->basename;
}
/**
* {@inheritdoc}
* @see FileInterface::getExtension()
*/
public function getExtension(bool $withDot = false): string
{
if (null === $this->extension) {
$this->setPathInfo();
}
return ($withDot ? '.' : '') . $this->extension;
}
/**
* {@inheritdoc}
* @see FileInterface::exists()
*/
public function exists(): bool
{
return is_file($this->filepath);
}
/**
* {@inheritdoc}
* @see FileInterface::getCreationTime()
*/
public function getCreationTime(): int
{
return is_file($this->filepath) ? filectime($this->filepath) : time();
}
/**
* {@inheritdoc}
* @see FileInterface::getModificationTime()
*/
public function getModificationTime(): int
{
return is_file($this->filepath) ? filemtime($this->filepath) : time();
}
/**
* {@inheritdoc}
* @see FileInterface::lock()
*/
public function lock(bool $block = true): bool
{
if (!$this->handle) {
if (!$this->mkdir($this->getPath())) {
throw new \RuntimeException('Creating directory failed for ' . $this->filepath);
}
$this->handle = @fopen($this->filepath, 'cb+');
if (!$this->handle) {
$error = error_get_last();
throw new \RuntimeException("Opening file for writing failed on error {$error['message']}");
}
}
$lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB;
// Some filesystems do not support file locks, only fail if another process holds the lock.
$this->locked = flock($this->handle, $lock, $wouldblock) || !$wouldblock;
return $this->locked;
}
/**
* {@inheritdoc}
* @see FileInterface::unlock()
*/
public function unlock(): bool
{
if (!$this->handle) {
return false;
}
if ($this->locked) {
flock($this->handle, LOCK_UN);
$this->locked = false;
}
fclose($this->handle);
$this->handle = null;
return true;
}
/**
* {@inheritdoc}
* @see FileInterface::isLocked()
*/
public function isLocked(): bool
{
return $this->locked;
}
/**
* {@inheritdoc}
* @see FileInterface::isReadable()
*/
public function isReadable(): bool
{
return is_readable($this->filepath) && is_file($this->filepath);
}
/**
* {@inheritdoc}
* @see FileInterface::isWritable()
*/
public function isWritable(): bool
{
if (!file_exists($this->filepath)) {
return $this->isWritablePath($this->getPath());
}
return is_writable($this->filepath) && is_file($this->filepath);
}
/**
* {@inheritdoc}
* @see FileInterface::load()
*/
public function load()
{
return file_get_contents($this->filepath);
}
/**
* {@inheritdoc}
* @see FileInterface::save()
*/
public function save($data): void
{
$filepath = $this->filepath;
$dir = $this->getPath();
if (!$this->mkdir($dir)) {
throw new \RuntimeException('Creating directory failed for ' . $filepath);
}
try {
if ($this->handle) {
$tmp = true;
// As we are using non-truncating locking, make sure that the file is empty before writing.
if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, $data) === false) {
// Writing file failed, throw an error.
$tmp = false;
}
} else {
// Create file with a temporary name and rename it to make the save action atomic.
$tmp = $this->tempname($filepath);
if (@file_put_contents($tmp, $data) === false) {
$tmp = false;
} elseif (@rename($tmp, $filepath) === false) {
@unlink($tmp);
$tmp = false;
}
}
} catch (\Exception $e) {
$tmp = false;
}
if ($tmp === false) {
throw new \RuntimeException('Failed to save file ' . $filepath);
}
// Touch the directory as well, thus marking it modified.
@touch($dir);
}
/**
* {@inheritdoc}
* @see FileInterface::rename()
*/
public function rename(string $path): bool
{
if ($this->exists() && !@rename($this->filepath, $path)) {
return false;
}
$this->setFilepath($path);
return true;
}
/**
* {@inheritdoc}
* @see FileInterface::delete()
*/
public function delete(): bool
{
return @unlink($this->filepath);
}
/**
* @param string $dir
* @return bool
* @throws \RuntimeException
* @internal
*/
protected function mkdir(string $dir): bool
{
// Silence error for open_basedir; should fail in mkdir instead.
if (@is_dir($dir)) {
return true;
}
$success = @mkdir($dir, 0777, true);
if (!$success) {
// Take yet another look, make sure that the folder doesn't exist.
clearstatcache(true, $dir);
if (!@is_dir($dir)) {
return false;
}
}
return true;
}
/**
* @return array
*/
protected function doSerialize(): array
{
return [
'filepath' => $this->filepath
];
}
/**
* @param array $serialized
*/
protected function doUnserialize(array $serialized): void
{
$this->setFilepath($serialized['filepath']);
}
/**
* @param string $filepath
*/
protected function setFilepath(string $filepath): void
{
$this->filepath = $filepath;
$this->filename = null;
$this->basename = null;
$this->path = null;
$this->extension = null;
}
protected function setPathInfo(): void
{
$pathInfo = $this->filesystem->pathinfo($this->filepath);
$this->filename = $pathInfo['filename'] ?? null;
$this->basename = $pathInfo['basename'] ?? null;
$this->path = $pathInfo['dirname'] ?? null;
$this->extension = $pathInfo['extension'] ?? null;
}
/**
* @param string $dir
* @return bool
* @internal
*/
protected function isWritablePath(string $dir): bool
{
if ($dir === '') {
return false;
}
if (!file_exists($dir)) {
// Recursively look up in the directory tree.
return $this->isWritablePath($this->filesystem->parent($dir));
}
return is_dir($dir) && is_writable($dir);
}
/**
* @param string $filename
* @param int $length
* @return string
*/
protected function tempname(string $filename, int $length = 5)
{
do {
$test = $filename . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
} while (file_exists($test));
return $test;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Formatter\CsvFormatter;
/**
* Class IniFile
* @package RocketTheme\Toolbox\File
*/
class CsvFile extends DataFile
{
/**
* File constructor.
* @param string $filepath
* @param CsvFormatter $formatter
*/
public function __construct($filepath, CsvFormatter $formatter)
{
parent::__construct($filepath, $formatter);
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
use RuntimeException;
class DataFile extends AbstractFile
{
/** @var FileFormatterInterface */
protected $formatter;
/**
* File constructor.
* @param string $filepath
* @param FileFormatterInterface $formatter
*/
public function __construct($filepath, FileFormatterInterface $formatter)
{
parent::__construct($filepath);
$this->formatter = $formatter;
}
/**
* {@inheritdoc}
* @see FileInterface::load()
*/
public function load()
{
$raw = parent::load();
try {
return $raw !== false ? $this->formatter->decode($raw) : false;
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
* @see FileInterface::save()
*/
public function save($data): void
{
if (\is_string($data)) {
// Make sure that the string is valid data.
try {
$this->formatter->decode($data);
} catch (RuntimeException $e) {
throw new RuntimeException(sprintf("Failed to save file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
}
$encoded = $data;
} else {
$encoded = $this->formatter->encode($data);
}
parent::save($encoded);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
class File extends AbstractFile
{
/**
* {@inheritdoc}
* @see FileInterface::load()
*/
public function load()
{
return parent::load();
}
/**
* {@inheritdoc}
* @see FileInterface::save()
*/
public function save($data): void
{
if (!\is_string($data)) {
throw new \RuntimeException('Cannot save data, string required');
}
parent::save($data);
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
/**
* Abstract file formatter.
*
* @package Grav\Framework\File\Formatter
*/
abstract class AbstractFormatter implements FileFormatterInterface
{
/** @var array */
private $config;
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config;
}
/**
* @return string
*/
public function serialize(): string
{
return serialize($this->doSerialize());
}
/**
* @param string $serialized
*/
public function unserialize($serialized): void
{
$this->doUnserialize(unserialize($serialized, ['allowed_classes' => false]));
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::getDefaultFileExtension()
*/
public function getDefaultFileExtension(): string
{
$extensions = $this->getSupportedFileExtensions();
// Call fails on bad configuration.
return reset($extensions);
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::getSupportedFileExtensions()
*/
public function getSupportedFileExtensions(): array
{
$extensions = $this->getConfig('file_extension');
// Call fails on bad configuration.
return \is_string($extensions) ? [$extensions] : $extensions;
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
abstract public function encode($data): string;
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
abstract public function decode($data);
/**
* Get either full configuration or a single option.
*
* @param string|null $name Configuration option (optional)
* @return mixed
*/
protected function getConfig(string $name = null)
{
if (null !== $name) {
return $this->config[$name] ?? null;
}
return $this->config;
}
/**
* @return array
*/
protected function doSerialize(): array
{
return ['config' => $this->config];
}
/**
* Note: if overridden, make sure you call parent::doUnserialize()
*
* @param array $serialized
*/
protected function doUnserialize(array $serialized): void
{
$this->config = $serialized['config'];
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
class CsvFormatter extends AbstractFormatter
{
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$config += [
'file_extension' => ['.csv', '.tsv'],
'delimiter' => ','
];
parent::__construct($config);
}
/**
* Returns delimiter used to both encode and decode CSV.
*
* @return string
*/
public function getDelimiter(): string
{
// Call fails on bad configuration.
return $this->getConfig('delimiter');
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function encode($data, $delimiter = null): string
{
if (count($data) === 0) {
return '';
}
$delimiter = $delimiter ?? $this->getDelimiter();
$header = array_keys(reset($data));
// Encode the field names
$string = $this->encodeLine($header, $delimiter);
// Encode the data
foreach ($data as $row) {
$string .= $this->encodeLine($row, $delimiter);
}
return $string;
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data, $delimiter = null): array
{
$delimiter = $delimiter ?? $this->getDelimiter();
$lines = preg_split('/\r\n|\r|\n/', $data);
if ($lines === false) {
throw new \RuntimeException('Decoding CSV failed');
}
// Get the field names
$header = str_getcsv(array_shift($lines), $delimiter);
// Get the data
$list = [];
foreach ($lines as $line) {
$list[] = array_combine($header, str_getcsv($line, $delimiter));
}
return $list;
}
protected function encodeLine(array $line, $delimiter = null): string
{
foreach ($line as $key => &$value) {
$value = $this->escape((string)$value);
}
unset($value);
return implode($delimiter, $line). "\n";
}
protected function escape(string $value)
{
if (preg_match('/[,"\r\n]/u', $value)) {
$value = '"' . preg_replace('/"/', '""', $value) . '"';
}
return $value;
}
}
@@ -1,44 +1,10 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
interface FormatterInterface
{
/**
* Get default file extension from current formatter (with dot).
*
* Default file extension is the first defined extension.
*
* @return string File extension (can be empty).
*/
public function getDefaultFileExtension();
use Grav\Framework\File\Interfaces\FileFormatterInterface;
/**
* Get file extensions supported by current formatter (with dot).
*
* @return string[]
*/
public function getSupportedFileExtensions();
/**
* Encode data into a string.
*
* @param array $data
* @return string
*/
public function encode($data);
/**
* Decode a string into data.
*
* @param string $data
* @return array
*/
public function decode($data);
}
/**
* @deprecated 1.6 Use Grav\Framework\File\Interfaces\FileFormatterInterface instead
*/
interface FormatterInterface extends FileFormatterInterface {};
@@ -1,61 +1,38 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class IniFormatter implements FormatterInterface
{
/** @var array */
private $config;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
class IniFormatter extends AbstractFormatter
{
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.ini'
];
}
$config += [
'file_extension' => '.ini'
];
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
parent::__construct($config);
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
public function encode($data): string
{
$string = '';
foreach ($data as $key => $value) {
@@ -71,8 +48,9 @@ class IniFormatter implements FormatterInterface
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data)
public function decode($data): array
{
$decoded = @parse_ini_string($data);
@@ -1,64 +1,83 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class JsonFormatter implements FormatterInterface
{
/** @var array */
private $config;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
class JsonFormatter extends AbstractFormatter
{
public function __construct(array $config = [])
{
$this->config = $config + [
$config += [
'file_extension' => '.json',
'encode_options' => 0,
'decode_assoc' => true
'decode_assoc' => true,
'decode_depth' => 512,
'decode_options' => 0
];
parent::__construct($config);
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
* Returns options used in encode() function.
*
* @return int
*/
public function getFileExtension()
public function getEncodeOptions(): int
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getConfig('encode_options');
}
return $this->getDefaultFileExtension();
/**
* Returns options used in decode() function.
*
* @return int
*/
public function getDecodeOptions(): int
{
return $this->getConfig('decode_options');
}
/**
* Returns recursion depth used in decode() function.
*
* @return int
*/
public function getDecodeDepth(): int
{
return $this->getConfig('decode_depth');
}
/**
* Returns true if JSON objects will be converted into associative arrays.
*
* @return bool
*/
public function getDecodeAssoc(): bool
{
return $this->getConfig('decode_assoc');
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function getDefaultFileExtension()
public function encode($data): string
{
$extensions = $this->getSupportedFileExtensions();
$encoded = @json_encode($data, $this->getEncodeOptions());
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
$encoded = @json_encode($data, $this->config['encode_options']);
if ($encoded === false) {
throw new \RuntimeException('Encoding JSON failed');
if ($encoded === false && json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Encoding JSON failed: ' . json_last_error_msg());
}
return $encoded;
@@ -66,13 +85,14 @@ class JsonFormatter implements FormatterInterface
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data)
{
$decoded = @json_decode($data, $this->config['decode_assoc']);
$decoded = @json_decode($data, $this->getDecodeAssoc(), $this->getDecodeDepth(), $this->getDecodeOptions());
if ($decoded === false) {
throw new \RuntimeException('Decoding JSON failed');
if (null === $decoded && json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Decoding JSON failed: ' . json_last_error_msg());
}
return $decoded;
@@ -1,23 +1,26 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class MarkdownFormatter implements FormatterInterface
use Grav\Framework\File\Interfaces\FileFormatterInterface;
class MarkdownFormatter extends AbstractFormatter
{
/** @var array */
private $config;
/** @var FormatterInterface */
/** @var FileFormatterInterface */
private $headerFormatter;
public function __construct(array $config = [], FormatterInterface $headerFormatter = null)
public function __construct(array $config = [], FileFormatterInterface $headerFormatter = null)
{
$this->config = $config + [
$config += [
'file_extension' => '.md',
'header' => 'header',
'body' => 'markdown',
@@ -25,44 +28,59 @@ class MarkdownFormatter implements FormatterInterface
'yaml' => ['inline' => 20]
];
$this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']);
parent::__construct($config);
$this->headerFormatter = $headerFormatter ?: new YamlFormatter($config['yaml']);
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
* Returns header field used in both encode() and decode().
*
* @return string
*/
public function getFileExtension()
public function getHeaderField(): string
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getConfig('header');
}
return $this->getDefaultFileExtension();
/**
* Returns body field used in both encode() and decode().
*
* @return string
*/
public function getBodyField(): string
{
return $this->getConfig('body');
}
/**
* Returns raw field used in both encode() and decode().
*
* @return string
*/
public function getRawField(): string
{
return $this->getConfig('raw');
}
/**
* Returns header formatter object used in both encode() and decode().
*
* @return FileFormatterInterface
*/
public function getHeaderFormatter(): FileFormatterInterface
{
return $this->headerFormatter;
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function getDefaultFileExtension()
public function encode($data): string
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$headerVar = $this->getHeaderField();
$bodyVar = $this->getBodyField();
$header = isset($data[$headerVar]) ? (array) $data[$headerVar] : [];
$body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : '';
@@ -70,7 +88,7 @@ class MarkdownFormatter implements FormatterInterface
// Create Markdown file with YAML header.
$encoded = '';
if ($header) {
$encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n";
$encoded = "---\n" . trim($this->getHeaderFormatter()->encode($data['header'])) . "\n---\n\n";
}
$encoded .= $body;
@@ -82,13 +100,15 @@ class MarkdownFormatter implements FormatterInterface
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data)
public function decode($data): array
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$rawVar = $this->config['raw'];
$headerVar = $this->getHeaderField();
$bodyVar = $this->getBodyField();
$rawVar = $this->getRawField();
// Define empty content
$content = [
$headerVar => [],
$bodyVar => ''
@@ -109,7 +129,7 @@ class MarkdownFormatter implements FormatterInterface
if ($rawVar) {
$content[$rawVar] = $frontmatter;
}
$content[$headerVar] = $this->headerFormatter->decode($frontmatter);
$content[$headerVar] = $this->getHeaderFormatter()->decode($frontmatter);
$content[$bodyVar] = $matches[2];
}
@@ -1,73 +1,64 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class SerializeFormatter implements FormatterInterface
{
/** @var array */
private $config;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
class SerializeFormatter extends AbstractFormatter
{
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.ser'
];
$config += [
'file_extension' => '.ser',
'decode_options' => ['allowed_classes' => [\stdClass::class]]
];
parent::__construct($config);
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
* Returns options used in decode().
*
* By default only allow stdClass class.
*
* @return array|bool
*/
public function getFileExtension()
public function getOptions()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
return $this->getConfig('decode_options');
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
public function encode($data): string
{
return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r']));
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data)
{
$decoded = @unserialize($data);
$decoded = @unserialize($data, $this->getOptions());
if ($decoded === false) {
if ($decoded === false && $data !== serialize(false)) {
throw new \RuntimeException('Decoding serialized data failed');
}
@@ -82,11 +73,11 @@ class SerializeFormatter implements FormatterInterface
* @param array $replace
* @return mixed
*/
protected function preserveLines($data, $search, $replace)
protected function preserveLines($data, array $search, array $replace)
{
if (is_string($data)) {
if (\is_string($data)) {
$data = str_replace($search, $replace, $data);
} elseif (is_array($data)) {
} elseif (\is_array($data)) {
foreach ($data as &$value) {
$value = $this->preserveLines($value, $search, $replace);
}
@@ -1,72 +1,80 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml as YamlParser;
use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYamlParser;
class YamlFormatter implements FormatterInterface
class YamlFormatter extends AbstractFormatter
{
/** @var array */
private $config;
public function __construct(array $config = [])
{
$this->config = $config + [
$config += [
'file_extension' => '.yaml',
'inline' => 5,
'indent' => 2,
'native' => true,
'compat' => true
];
parent::__construct($config);
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
* @return int
*/
public function getFileExtension()
public function getInlineOption(): int
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getConfig('inline');
}
return $this->getDefaultFileExtension();
/**
* @return int
*/
public function getIndentOption(): int
{
return $this->getConfig('indent');
}
/**
* @return bool
*/
public function useNativeDecoder(): bool
{
return $this->getConfig('native');
}
/**
* @return bool
*/
public function useCompatibleDecoder(): bool
{
return $this->getConfig('compat');
}
/**
* {@inheritdoc}
* @see FileFormatterInterface::encode()
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data, $inline = null, $indent = null)
public function encode($data, $inline = null, $indent = null): string
{
try {
return (string) YamlParser::dump(
return YamlParser::dump(
$data,
$inline ? (int) $inline : $this->config['inline'],
$indent ? (int) $indent : $this->config['indent'],
$inline ? (int) $inline : $this->getInlineOption(),
$indent ? (int) $indent : $this->getIndentOption(),
YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE
);
} catch (DumpException $e) {
@@ -76,14 +84,15 @@ class YamlFormatter implements FormatterInterface
/**
* {@inheritdoc}
* @see FileFormatterInterface::decode()
*/
public function decode($data)
public function decode($data): array
{
// Try native PECL YAML PHP extension first if available.
if ($this->config['native'] && function_exists('yaml_parse')) {
if (\function_exists('yaml_parse') && $this->useNativeDecoder()) {
// Safely decode YAML.
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
@ini_set('yaml.decode_php', '0');
$decoded = @yaml_parse($data);
@ini_set('yaml.decode_php', $saved);
@@ -95,7 +104,7 @@ class YamlFormatter implements FormatterInterface
try {
return (array) YamlParser::parse($data);
} catch (ParseException $e) {
if ($this->config['compat']) {
if ($this->useCompatibleDecoder()) {
return (array) FallbackYamlParser::parse($data);
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Formatter\IniFormatter;
/**
* Class IniFile
* @package RocketTheme\Toolbox\File
*/
class IniFile extends DataFile
{
/**
* File constructor.
* @param string $filepath
* @param IniFormatter $formatter
*/
public function __construct($filepath, IniFormatter $formatter)
{
parent::__construct($filepath, $formatter);
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Interfaces;
/**
* Defines common interface for all file formatters.
*
* File formatters allow you to read and optionally write various file formats, such as:
*
* @used-by \Grav\Framework\File\Formatter\CsvFormatter CVS
* @used-by \Grav\Framework\File\Formatter\JsonFormatter JSON
* @used-by \Grav\Framework\File\Formatter\MarkdownFormatter Markdown
* @used-by \Grav\Framework\File\Formatter\SerializeFormatter Serialized PHP
* @used-by \Grav\Framework\File\Formatter\YamlFormatter YAML
*
* @since 1.6
*/
interface FileFormatterInterface extends \Serializable
{
/**
* Get default file extension from current formatter (with dot).
*
* Default file extension is the first defined extension.
*
* @return string Returns file extension (can be empty).
* @api
*/
public function getDefaultFileExtension(): string;
/**
* Get file extensions supported by current formatter (with dot).
*
* @return string[] Returns list of all supported file extensions.
* @api
*/
public function getSupportedFileExtensions(): array;
/**
* Encode data into a string.
*
* @param mixed $data Data to be encoded.
*
* @return string Returns encoded data as a string.
* @api
*/
public function encode($data): string;
/**
* Decode a string into data.
*
* @param string $data String to be decoded.
*
* @return mixed Returns decoded data.
* @api
*/
public function decode($data);
}
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Interfaces;
/**
* Defines common interface for all file readers.
*
* File readers allow you to read and optionally write files of various file formats, such as:
*
* @used-by \Grav\Framework\File\CsvFile CVS
* @used-by \Grav\Framework\File\JsonFile JSON
* @used-by \Grav\Framework\File\MarkdownFile Markdown
* @used-by \Grav\Framework\File\SerializeFile Serialized PHP
* @used-by \Grav\Framework\File\YamlFile YAML
*
* @since 1.6
*/
interface FileInterface extends \Serializable
{
/**
* Get both path and filename of the file.
*
* @return string Returns path and filename in the filesystem. Can also be URI.
* @api
*/
public function getFilePath(): string;
/**
* Get path of the file.
*
* @return string Returns path in the filesystem. Can also be URI.
* @api
*/
public function getPath(): string;
/**
* Get filename of the file.
*
* @return string Returns name of the file.
* @api
*/
public function getFilename(): string;
/**
* Get basename of the file (filename without the associated file extension).
*
* @return string Returns basename of the file.
* @api
*/
public function getBasename(): string;
/**
* Get file extension of the file.
*
* @param bool $withDot If true, return file extension with beginning dot (.json).
*
* @return string Returns file extension of the file (can be empty).
* @api
*/
public function getExtension(bool $withDot = false): string;
/**
* Check if the file exits in the filesystem.
*
* @return bool Returns `true` if the filename exists and is a regular file, `false` otherwise.
* @api
*/
public function exists(): bool;
/**
* Get file creation time.
*
* @return int Returns Unix timestamp. If file does not exist, method returns current time.
* @api
*/
public function getCreationTime(): int;
/**
* Get file modification time.
*
* @return int Returns Unix timestamp. If file does not exist, method returns current time.
* @api
*/
public function getModificationTime(): int;
/**
* Lock file for writing. You need to manually call unlock().
*
* @param bool $block For non-blocking lock, set the parameter to `false`.
*
* @return bool Returns `true` if the file was successfully locked, `false` otherwise.
* @throws \RuntimeException
* @api
*/
public function lock(bool $block = true): bool;
/**
* Unlock file after writing.
*
* @return bool Returns `true` if the file was successfully unlocked, `false` otherwise.
* @api
*/
public function unlock(): bool;
/**
* Returns true if file has been locked by you for writing.
*
* @return bool Returns `true` if the file is locked, `false` otherwise.
* @api
*/
public function isLocked(): bool;
/**
* Check if file exists and can be read.
*
* @return bool Returns `true` if the file can be read, `false` otherwise.
* @api
*/
public function isReadable(): bool;
/**
* Check if file can be written.
*
* @return bool Returns `true` if the file can be written, `false` otherwise.
* @api
*/
public function isWritable(): bool;
/**
* (Re)Load a file and return file contents.
*
* @return string|array|object|false Returns file content or `false` if file couldn't be read.
* @api
*/
public function load();
/**
* Save file.
*
* See supported data format for each of the file format.
*
* @param mixed $data Data to be saved.
*
* @throws \RuntimeException
* @api
*/
public function save($data): void;
/**
* Rename file in the filesystem if it exists.
*
* Target folder will be created if if did not exist.
*
* @param string $path New path and filename for the file. Can also be URI.
*
* @return bool Returns `true` if the file was successfully renamed, `false` otherwise.
* @api
*/
public function rename(string $path): bool;
/**
* Delete file from filesystem.
*
* @return bool Returns `true` if the file was successfully deleted, `false` otherwise.
* @api
*/
public function delete(): bool;
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Formatter\JsonFormatter;
/**
* Class JsonFile
* @package Grav\Framework\File
*/
class JsonFile extends DataFile
{
/**
* File constructor.
* @param string $filepath
* @param JsonFormatter $formatter
*/
public function __construct($filepath, JsonFormatter $formatter)
{
parent::__construct($filepath, $formatter);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Formatter\MarkdownFormatter;
/**
* Class MarkdownFile
* @package Grav\Framework\File
*/
class MarkdownFile extends DataFile
{
/**
* File constructor.
* @param string $filepath
* @param MarkdownFormatter $formatter
*/
public function __construct($filepath, MarkdownFormatter $formatter)
{
parent::__construct($filepath, $formatter);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\File
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File;
use Grav\Framework\File\Formatter\YamlFormatter;
/**
* Class YamlFile
* @package Grav\Framework\File
*/
class YamlFile extends DataFile
{
/**
* File constructor.
* @param string $filepath
* @param YamlFormatter $formatter
*/
public function __construct($filepath, YamlFormatter $formatter)
{
parent::__construct($filepath, $formatter);
}
}
@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Filesystem
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Filesystem;
use Grav\Framework\Filesystem\Interfaces\FilesystemInterface;
class Filesystem implements FilesystemInterface
{
/** @var bool|null */
private $normalize;
/** @var static */
static protected $default;
/** @var static */
static protected $unsafe;
/** @var static */
static protected $safe;
/**
* @param bool|null $normalize See $this->setNormalization()
*
* @return Filesystem
*/
public static function getInstance(bool $normalize = null): Filesystem
{
if ($normalize === true) {
$instance = &static::$safe;
} elseif ($normalize === false) {
$instance = &static::$unsafe;
} else {
$instance = &static::$default;
}
if (null === $instance) {
$instance = new static($normalize);
}
return $instance;
}
/**
* Always use Filesystem::getInstance() instead.
*
* @param bool|null $normalize
*/
protected function __construct(bool $normalize = null)
{
$this->normalize = $normalize;
}
/**
* Set path normalization.
*
* Default option enables normalization for the streams only, but you can force the normalization to be either
* on or off for every path. Disabling path normalization speeds up the calls, but may cause issues if paths were
* not normalized.
*
* @param bool|null $normalize
*
* @return Filesystem
*/
public function setNormalization(bool $normalize = null): self
{
return static::getInstance($normalize);
}
/**
* Force all paths to be normalized.
*
* @return static
*/
public function unsafe(): self
{
return static::getInstance(true);
}
/**
* Force all paths not to be normalized (speeds up the calls if given paths are known to be normalized).
*
* @return static
*/
public function safe(): self
{
return static::getInstance(false);
}
/**
* {@inheritdoc}
* @see FilesystemInterface::parent()
*/
public function parent(string $path, int $levels = 1): string
{
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
if ($this->normalize !== false) {
$path = $this->normalizePathPart($path);
}
if ($path === '' || $path === '.') {
return '';
}
[$scheme, $parent] = $this->dirnameInternal($scheme, $path, $levels);
return $parent !== $path ? $this->toString($scheme, $parent) : '';
}
/**
* {@inheritdoc}
* @see FilesystemInterface::normalize()
*/
public function normalize(string $path): string
{
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
$path = $this->normalizePathPart($path);
return $this->toString($scheme, $path);
}
/**
* {@inheritdoc}
* @see FilesystemInterface::dirname()
*/
public function dirname(string $path, int $levels = 1): string
{
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
if ($this->normalize || ($scheme && null === $this->normalize)) {
$path = $this->normalizePathPart($path);
}
[$scheme, $path] = $this->dirnameInternal($scheme, $path, $levels);
return $this->toString($scheme, $path);
}
/**
* {@inheritdoc}
* @see FilesystemInterface::pathinfo()
*/
public function pathinfo(string $path, int $options = null)
{
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
if ($this->normalize || ($scheme && null === $this->normalize)) {
$path = $this->normalizePathPart($path);
}
return $this->pathinfoInternal($scheme, $path, $options);
}
/**
* @param string|null $scheme
* @param string $path
* @param int $levels
*
* @return array
*/
protected function dirnameInternal(?string $scheme, string $path, int $levels = 1): array
{
$path = \dirname($path, $levels);
if (null !== $scheme && $path === '.') {
return [$scheme, ''];
}
return [$scheme, $path];
}
/**
* @param string|null $scheme
* @param string $path
* @param int|null $options
*
* @return array
*/
protected function pathinfoInternal(?string $scheme, string $path, int $options = null)
{
$info = $options ? \pathinfo($path, $options) : \pathinfo($path);
if (null !== $scheme) {
$info['scheme'] = $scheme;
$dirname = isset($info['dirname']) && $info['dirname'] !== '.' ? $info['dirname'] : null;
if (null !== $dirname) {
$info['dirname'] = $scheme . '://' . $dirname;
} else {
$info = ['dirname' => $scheme . '://'] + $info;
}
}
return $info;
}
/**
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)).
*
* @param string $filename
*
* @return array
*/
protected function getSchemeAndHierarchy(string $filename): array
{
$components = explode('://', $filename, 2);
return 2 === \count($components) ? $components : [null, $components[0]];
}
/**
* @param string|null $scheme
* @param string $path
*
* @return string
*/
protected function toString(?string $scheme, string $path): string
{
if ($scheme) {
return $scheme . '://' . $path;
}
return $path;
}
/**
* @param string $path
*
* @return string
* @throws \RuntimeException
*/
protected function normalizePathPart(string $path): string
{
// Quick check for empty path.
if ($path === '' || $path === '.') {
return '';
}
// Quick check for root.
if ($path === '/') {
return '/';
}
// If the last character is not '/' or any of '\', './', '//' and '..' are not found, path is clean and we're done.
if ($path[-1] !== '/' && !preg_match('`(\\\\|\./|//|\.\.)`', $path)) {
return $path;
}
// Convert backslashes
$path = strtr($path, ['\\' => '/']);
$parts = explode('/', $path);
// Keep absolute paths.
$root = '';
if ($parts[0] === '') {
$root = '/';
array_shift($parts);
}
$list = [];
foreach ($parts as $i => $part) {
// Remove empty parts: // and /./
if ($part === '' || $part === '.') {
continue;
}
// Resolve /../ by removing path part.
if ($part === '..') {
$test = array_shift($list);
if ($test === null) {
// Oops, user tried to access something outside of our root folder.
throw new \RuntimeException("Bad path {$path}");
}
}
$list[] = $part;
}
// Build path back together.
return $root . implode('/', $list);
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Filesystem
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Filesystem\Interfaces;
use Grav\Framework\Filesystem\Filesystem;
/**
* Defines several stream-save filesystem actions.
*
* @used-by Filesystem
* @since 1.6
*/
interface FilesystemInterface
{
/**
* Get parent path. Empty path is returned if there are no segments remaining.
*
* Can be used recursively to get towards the root directory.
*
* @param string $path A filename or path, does not need to exist as a file.
* @param int $levels The number of parent directories to go up (>= 1).
*
* @return string Returns parent path.
* @throws \RuntimeException
* @api
*/
public function parent(string $path, int $levels = 1): string;
/**
* Normalize path by cleaning up `\`, `/./`, `//` and `/../`.
*
* @param string $path A filename or path, does not need to exist as a file.
*
* @return string Returns normalized path.
* @throws \RuntimeException
* @api
*/
public function normalize(string $path): string;
/**
* Stream-safe `\dirname()` replacement.
*
* @see http://php.net/manual/en/function.dirname.php
*
* @param string $path A filename or path, does not need to exist as a file.
* @param int $levels The number of parent directories to go up (>= 1).
*
* @return string Returns path to the directory.
* @throws \RuntimeException
* @api
*/
public function dirname(string $path, int $levels = 1): string;
/**
* Stream-safe `\pathinfo()` replacement.
*
* @see http://php.net/manual/en/function.pathinfo.php
*
* @param string $path A filename or path, does not need to exist as a file.
* @param int $options A PATHINFO_* constant.
*
* @return array|string
* @api
*/
public function pathinfo(string $path, int $options = null);
}
+311
View File
@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\ObjectCollection;
/**
* Class Flex
* @package Grav\Framework\Flex
*/
class Flex implements \Countable
{
/** @var array */
protected $config;
/** @var FlexDirectory[] */
protected $types;
/**
* Flex constructor.
* @param array $types List of [type => blueprint file, ...]
* @param array $config
*/
public function __construct(array $types, array $config)
{
$this->config = $config;
$this->types = [];
foreach ($types as $type => $blueprint) {
$this->addDirectoryType($type, $blueprint);
}
}
/**
* @param string $type
* @param string $blueprint
* @param array $config
* @return $this
*/
public function addDirectoryType(string $type, string $blueprint, array $config = [])
{
$config = array_merge_recursive(['enabled' => true], $this->config['object'] ?? [], $config);
$this->types[$type] = new FlexDirectory($type, $blueprint, $config);
return $this;
}
/**
* @param FlexDirectory $directory
* @return $this
*/
public function addDirectory(FlexDirectory $directory)
{
$this->types[$directory->getFlexType()] = $directory;
return $this;
}
/**
* @param string $type
* @return bool
*/
public function hasDirectory(string $type): bool
{
return isset($this->types[$type]);
}
/**
* @param array|string[] $types
* @param bool $keepMissing
* @return array|FlexDirectory[]
*/
public function getDirectories(array $types = null, bool $keepMissing = false): array
{
if ($types === null) {
return $this->types;
}
// Return the directories in the given order.
$directories = [];
foreach ($types as $type) {
$directories[$type] = $this->types[$type] ?? null;
}
return $keepMissing ? $directories : array_filter($directories);
}
/**
* @param string $type
* @return FlexDirectory|null
*/
public function getDirectory(string $type): ?FlexDirectory
{
return $this->types[$type] ?? null;
}
/**
* @param string $type
* @param array|null $keys
* @param string|null $keyField
* @return FlexCollectionInterface|null
*/
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
{
$directory = $type ? $this->getDirectory($type) : null;
return $directory ? $directory->getCollection($keys, $keyField) : null;
}
/**
* @param array $keys
* @param array $options In addition to the options in getObjects(), following options can be passed:
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
* @return FlexCollectionInterface
* @throws \RuntimeException
*/
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
{
$collectionClass = $options['collection_class'] ?? ObjectCollection::class;
if (!class_exists($collectionClass)) {
throw new \RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass));
}
$objects = $this->getObjects($keys, $options);
return new $collectionClass($objects);
}
/**
* @param array $keys
* @param array $options Following optional options can be passed:
* types: List of allowed types.
* type: Allowed type if types isn't defined, otherwise acts as default_type.
* default_type: Set default type for objects given without type (only used if key_field isn't set).
* keep_missing: Set to true if you want to return missing objects as null.
* key_field: Key field which is used to match the objects.
* @return array
*/
public function getObjects(array $keys, array $options = []): array
{
$type = $options['type'] ?? null;
$defaultType = $options['default_type'] ?? $type ?? null;
$keyField = $options['key_field'] ?? 'flex_key';
// Prepare empty result lists for all requested Flex types.
$types = $options['types'] ?? (array)$type ?: null;
if ($types) {
$types = array_fill_keys($types, []);
}
$strict = isset($types);
$guessed = [];
if ($keyField === 'flex_key') {
// We need to split Flex key lookups into individual directories.
$undefined = [];
$keyFieldFind = 'storage_key';
foreach ($keys as $flexKey) {
if (!$flexKey) {
continue;
}
$flexKey = (string)$flexKey;
// Normalize key and type using fallback to default type if it was set.
[$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType);
if ($type === '' && $types) {
// Add keys which are not associated to any Flex type. They will be included to every Flex type.
foreach ($types as $type => &$array) {
$array[] = $key;
$guessed[$key][] = "{$type}.obj:{$key}";
}
unset($array);
} elseif (!$strict || isset($types[$type])) {
// Collect keys by their Flex type. If allowed types are defined, only include values from those types.
$types[$type][] = $key;
if ($guess) {
$guessed[$key][] = "{$type}.obj:{$key}";
}
}
}
} else {
// We are using a specific key field, make every key undefined.
$undefined = $keys;
$keyFieldFind = $keyField;
}
if (!$types) {
return [];
}
$list = [[]];
foreach ($types as $type => $typeKeys) {
// Also remember to look up keys from undefined Flex types.
$lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys;
$collection = $this->getCollection($type, $lookupKeys, $keyFieldFind);
if ($collection && $keyFieldFind !== $keyField) {
$collection = $collection->withKeyField($keyField);
}
$list[] = $collection ? $collection->toArray() : [];
}
// Merge objects from individual types back together.
$list = array_merge(...$list);
// Use the original key ordering.
if (!$guessed) {
$list = array_replace(array_fill_keys($keys, null), $list);
} else {
// We have mixed keys, we need to map flex keys back to storage keys.
$results = [];
foreach ($keys as $key) {
$flexKey = $guessed[$key] ?? $key;
if (\is_array($flexKey)) {
$result = null;
foreach ($flexKey as $tryKey) {
if ($result = $list[$tryKey] ?? null) {
// Use the first matching object (conflicting objects will be ignored for now).
break;
}
}
} else {
$result = $list[$flexKey] ?? null;
}
$results[$key] = $result;
}
$list = $results;
}
// Remove missing objects if not asked to keep them.
if (empty($option['keep_missing'])) {
$list = array_filter($list);
}
return $list;
}
/**
* @param string $key
* @param string|null $type
* @param string|null $keyField
* @return FlexObjectInterface|null
*/
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
{
if (null === $type && null === $keyField) {
// Special handling for quick Flex key lookups.
$keyField = 'storage_key';
[$type, $key] = $this->resolveKeyAndType($key, $type);
} else {
$type = $this->resolveType($type);
}
if ($type === '' || $key === '') {
return null;
}
$directory = $this->getDirectory($type);
return $directory ? $directory->getObject($key, $keyField) : null;
}
/**
* @return int
*/
public function count(): int
{
return \count($this->types);
}
protected function resolveKeyAndType(string $flexKey, string $type = null): array
{
$guess = false;
if (strpos($flexKey, ':') !== false) {
[$type, $key] = explode(':', $flexKey, 2);
$type = $this->resolveType($type);
} else {
$key = $flexKey;
$type = (string)$type;
$guess = true;
}
return [$key, $type, $guess];
}
protected function resolveType(string $type = null): string
{
if (null !== $type && strpos($type, '.') !== false) {
return preg_replace('|\.obj$|', '', $type);
}
return $type ?? '';
}
}
@@ -0,0 +1,525 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\ContentBlock\ContentBlockInterface;
use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\ObjectCollection;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Psr\SimpleCache\InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\TemplateWrapper;
/**
* Class FlexCollection
* @package Grav\Framework\Flex
*/
class FlexCollection extends ObjectCollection implements FlexCollectionInterface
{
/** @var FlexDirectory */
private $_flexDirectory;
/** @var string */
private $_keyField;
/**
* Get list of cached methods.
*
* @return array Returns a list of methods with their caching information.
*/
public static function getCachedMethods(): array
{
return [
'getTypePrefix' => true,
'getType' => true,
'getFlexDirectory' => true,
'getCacheKey' => true,
'getCacheChecksum' => true,
'getTimestamp' => true,
'hasProperty' => true,
'getProperty' => true,
'hasNestedProperty' => true,
'getNestedProperty' => true,
'orderBy' => true,
'render' => false,
'isAuthorized' => 'session',
'search' => true,
'sort' => true,
];
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::createFromArray()
*/
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
{
$instance = new static($entries, $directory);
$instance->setKeyField($keyField);
return $instance;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::__construct()
*/
public function __construct(array $entries = [], FlexDirectory $directory = null)
{
parent::__construct($entries);
if ($directory) {
$this->setFlexDirectory($directory)->setKey($directory->getFlexType());
}
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::search()
*/
public function search(string $search, $properties = null, array $options = null)
{
$matching = $this->call('search', [$search, $properties, $options]);
$matching = array_filter($matching);
if ($matching) {
uksort($matching, function ($a, $b) {
return -($a <=> $b);
});
}
return $this->select(array_keys($matching));
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::sort()
*/
public function sort(array $order)
{
$criteria = Criteria::create()->orderBy($order);
/** @var FlexCollectionInterface $matching */
$matching = $this->matching($criteria);
return $matching;
}
/**
* @param array $filters
* @return FlexCollectionInterface|Collection
*/
public function filterBy(array $filters)
{
$expr = Criteria::expr();
$criteria = Criteria::create();
foreach ($filters as $key => $value) {
$criteria->andWhere($expr->eq($key, $value));
}
return $this->matching($criteria);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexType()
*/
public function getFlexType(): string
{
return $this->_flexDirectory->getFlexType();
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getFlexDirectory(): FlexDirectory
{
return $this->_flexDirectory;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getTimestamp()
*/
public function getTimestamp(): int
{
$timestamps = $this->getTimestamps();
return $timestamps ? max($timestamps) : time();
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getCacheKey(): string
{
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->call('getKey')));
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getCacheChecksum(): string
{
return sha1(json_encode($this->getTimestamps()));
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getTimestamps(): array
{
/** @var int[] $timestamps */
$timestamps = $this->call('getTimestamp');
return $timestamps;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getStorageKeys(): array
{
/** @var string[] $keys */
$keys = $this->call('getStorageKey');
return $keys;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getFlexKeys(): array
{
/** @var string[] $keys */
$keys = $this->call('getFlexKey');
return $keys;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::withKeyField()
*/
public function withKeyField(string $keyField = null)
{
$keyField = $keyField ?: 'key';
if ($keyField === $this->getKeyField()) {
return $this;
}
$entries = [];
foreach ($this as $key => $object) {
// TODO: remove hardcoded logic
if ($keyField === 'storage_key') {
$entries[$object->getStorageKey()] = $object;
} elseif ($keyField === 'flex_key') {
$entries[$object->getFlexKey()] = $object;
} elseif ($keyField === 'key') {
$entries[$object->getKey()] = $object;
}
}
return $this->createFrom($entries, $keyField);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getIndex()
*/
public function getIndex()
{
return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::render()
*/
public function render(string $layout = null, array $context = [])
{
if (null === $layout) {
$layout = 'default';
}
$type = $this->getFlexType();
$grav = Grav::instance();
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
$cache = $key = null;
foreach ($context as $value) {
if (!\is_scalar($value)) {
$key = false;
}
}
if ($key !== false) {
$key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
$cache = $this->getCache('render');
}
try {
$data = $cache ? $cache->get($key) : null;
$block = $data ? HtmlBlock::fromArray($data) : null;
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
$block = null;
} catch (\InvalidArgumentException $e) {
$debugger->addException($e);
$block = null;
}
$checksum = $this->getCacheChecksum();
if ($block && $checksum !== $block->getChecksum()) {
$block = null;
}
if (!$block) {
$block = HtmlBlock::create($key);
$block->setChecksum($checksum);
if ($key === false) {
$block->disableCache();
}
$grav->fireEvent('onFlexCollectionRender', new Event([
'collection' => $this,
'layout' => &$layout,
'context' => &$context
]));
$output = $this->getTemplate($layout)->render(
['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'collection' => $this, 'layout' => $layout] + $context
);
if ($debugger->enabled()) {
$output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
}
$block->setContent($output);
try {
$cache && $block->isCached() && $cache->set($key, $block->toArray());
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
}
}
$debugger->stopTimer('flex-collection-' . $debugKey);
return $block;
}
/**
* @param bool $prefix
* @return string
* @deprecated 1.6 Use `->getFlexType()` instead.
*/
public function getType($prefix = false)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
$type = $prefix ? $this->getTypePrefix() : '';
return $type . $this->getFlexType();
}
/**
* @param FlexDirectory $type
* @return $this
*/
public function setFlexDirectory(FlexDirectory $type)
{
$this->_flexDirectory = $type;
return $this;
}
/**
* @return array
*/
public function getMetaData(string $key) : array
{
$object = $this->get($key);
return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
}
/**
* @param string|null $namespace
* @return CacheInterface
*/
public function getCache(string $namespace = null)
{
return $this->_flexDirectory->getCache($namespace);
}
/**
* @return string
*/
public function getKeyField(): string
{
return $this->_keyField ?? 'storage_key';
}
/**
* @param string $action
* @param string|null $scope
* @param UserInterface|null $user
* @return static
*/
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
{
$list = $this->call('isAuthorized', [$action, $scope, $user]);
$list = \array_filter($list);
return $this->select(array_keys($list));
}
/**
* @param string $value
* @param string $field
* @return FlexObject|null
*/
public function find($value, $field = 'id')
{
if ($value) foreach ($this as $element) {
if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
return $element;
}
}
return null;
}
/**
* @return array
*/
public function jsonSerialize()
{
$elements = [];
/**
* @var string $key
* @var array|FlexObject $object
*/
foreach ($this->getElements() as $key => $object) {
$elements[$key] = \is_array($object) ? $object : $object->jsonSerialize();
}
return $elements;
}
public function __debugInfo()
{
return [
'type:private' => $this->getFlexType(),
'key:private' => $this->getKey(),
'objects_key:private' => $this->getKeyField(),
'objects:private' => $this->getElements()
];
}
/**
* Creates a new instance from the specified elements.
*
* This method is provided for derived classes to specify how a new
* instance should be created when constructor semantics have changed.
*
* @param array $elements Elements.
* @param string|null $keyField
*
* @return static
* @throws \InvalidArgumentException
*/
protected function createFrom(array $elements, $keyField = null)
{
$collection = new static($elements, $this->_flexDirectory);
$collection->setKeyField($keyField ?: $this->_keyField);
return $collection;
}
/**
* @return string
*/
protected function getTypePrefix(): string
{
return 'c.';
}
/**
* @param string $layout
* @return TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
protected function getTemplate($layout)
{
$grav = Grav::instance();
/** @var Twig $twig */
$twig = $grav['twig'];
try {
return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/collection/{$layout}.html.twig",
"flex-objects/layouts/_default/collection/{$layout}.html.twig"
]
);
} catch (LoaderError $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']);
}
}
/**
* @param string $type
* @return FlexDirectory
*/
protected function getRelatedDirectory($type): ?FlexDirectory
{
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
return $flex->getDirectory($type);
}
protected function setKeyField($keyField = null): void
{
$this->_keyField = $keyField ?? 'storage_key';
}
}
@@ -0,0 +1,669 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Framework\Cache\Adapter\DoctrineCache;
use Grav\Framework\Cache\Adapter\MemoryCache;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Grav\Framework\Flex\Storage\SimpleStorage;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Psr\SimpleCache\InvalidArgumentException;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
/**
* Class FlexDirectory
* @package Grav\Framework\Flex
*/
class FlexDirectory implements FlexAuthorizeInterface
{
use FlexAuthorizeTrait;
/** @var string */
protected $type;
/** @var string */
protected $blueprint_file;
/** @var Blueprint[] */
protected $blueprints;
/** @var bool[] */
protected $blueprints_init;
/** @var FlexIndexInterface|null */
protected $index;
/** @var FlexCollectionInterface|null */
protected $collection;
/** @var bool */
protected $enabled;
/** @var array */
protected $defaults;
/** @var Config */
protected $config;
/** @var FlexStorageInterface */
protected $storage;
/** @var CacheInterface */
protected $cache;
/** @var string */
protected $objectClassName;
/** @var string */
protected $collectionClassName;
/** @var string */
protected $indexClassName;
/**
* FlexDirectory constructor.
* @param string $type
* @param string $blueprint_file
* @param array $defaults
*/
public function __construct(string $type, string $blueprint_file, array $defaults = [])
{
$this->type = $type;
$this->blueprints = [];
$this->blueprint_file = $blueprint_file;
$this->defaults = $defaults;
$this->enabled = !empty($defaults['enabled']);
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* @return string
* @deprecated 1.6 Use ->getFlexType() method instead.
*/
public function getType(): string
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
return $this->type;
}
/**
* @return string
*/
public function getFlexType(): string
{
return $this->type;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType()));
}
/**
* @return string
*/
public function getDescription(): string
{
return $this->getBlueprintInternal()->get('description', '');
}
/**
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function getConfig(string $name = null, $default = null)
{
if (null === $this->config) {
$this->config = new Config(array_merge_recursive($this->getBlueprintInternal()->get('config', []), $this->defaults));
}
return null === $name ? $this->config : $this->config->get($name, $default);
}
/**
* @param string $type
* @param string $context
* @return Blueprint
*/
public function getBlueprint(string $type = '', string $context = '')
{
$blueprint = $this->getBlueprintInternal($type, $context);
if (empty($this->blueprints_init[$type])) {
$this->blueprints_init[$type] = true;
$blueprint->setScope('object');
$blueprint->init();
if (empty($blueprint->fields())) {
throw new RuntimeException(sprintf('Flex: Blueprint for %s is missing', $this->type));
}
}
return $blueprint;
}
/**
* @param string $view
* @return string
*/
public function getBlueprintFile(string $view = ''): string
{
$file = $this->blueprint_file;
if ($view !== '') {
$file = preg_replace('/\.yaml/', "/{$view}.yaml", $file);
}
return $file;
}
/**
* Get collection. In the site this will be filtered by the default filters (published etc).
*
* Use $directory->getIndex() if you want unfiltered collection.
*
* @param array|null $keys Array of keys.
* @param string|null $keyField Field to be used as the key.
* @return FlexCollectionInterface
*/
public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface
{
// Get all selected entries.
$index = $this->getIndex($keys, $keyField);
if (!Utils::isAdminPlugin()) {
// If not in admin, filter the list by using default filters.
$filters = (array)$this->getConfig('site.filter', []);
foreach ($filters as $filter) {
$index = $index->{$filter}();
}
}
return $index;
}
/**
* Get the full collection of all stored objects.
*
* Use $directory->getCollection() if you want a filtered collection.
*
* @param array|null $keys Array of keys.
* @param string|null $keyField Field to be used as the key.
* @return FlexIndexInterface
*/
public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface
{
$index = clone $this->loadIndex();
$index = $index->withKeyField($keyField);
if (null !== $keys) {
$index = $index->select($keys);
}
return $index->getIndex();
}
/**
* Returns an object if it exists.
*
* Note: It is not safe to use the object without checking if the user can access it.
*
* @param string $key
* @param string|null $keyField Field to be used as the key.
* @return FlexObjectInterface|null
*/
public function getObject($key, string $keyField = null): ?FlexObjectInterface
{
return $this->getIndex(null, $keyField)->get($key);
}
/**
* @param array $data
* @param string|null $key
* @return FlexObjectInterface
*/
public function update(array $data, string $key = null): FlexObjectInterface
{
$object = null !== $key ? $this->getIndex()->get($key): null;
$storage = $this->getStorage();
if (null === $object) {
$object = $this->createObject($data, $key, true);
$key = $object->getStorageKey();
if ($key) {
$rows = $storage->replaceRows([$key => $object->prepareStorage()]);
} else {
$rows = $storage->createRows([$object->prepareStorage()]);
}
} else {
$oldKey = $object->getStorageKey();
$object->update($data);
$newKey = $object->getStorageKey();
if ($oldKey !== $newKey) {
$object->triggerEvent('move');
$storage->renameRow($oldKey, $newKey);
// TODO: media support.
}
$object->save();
}
try {
$this->clearCache();
} catch (InvalidArgumentException $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
// Caching failed, but we can ignore that for now.
}
return $object;
}
/**
* @param string $key
* @return FlexObjectInterface|null
*/
public function remove(string $key): ?FlexObjectInterface
{
$object = $this->getIndex()->get($key);
if (!$object) {
return null;
}
$object->delete();
return $object;
}
/**
* @param string|null $namespace
* @return CacheInterface
*/
public function getCache(string $namespace = null)
{
$namespace = $namespace ?: 'index';
$cache = $this->cache[$namespace] ?? null;
if (null === $cache) {
try {
$grav = Grav::instance();
/** @var Cache $gravCache */
$gravCache = $grav['cache'];
$config = $this->getConfig('cache.' . $namespace);
if (empty($config['enabled'])) {
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
} else {
$timeout = $config['timeout'] ?? 60;
$key = $gravCache->getKey();
if (Utils::isAdminPlugin()) {
$key = substr($key, 0, -1);
}
$cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout);
}
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
}
// Disable cache key validation.
$cache->setValidation(false);
$this->cache[$namespace] = $cache;
}
return $cache;
}
/**
* @return $this
*/
public function clearCache()
{
$grav = Grav::instance();
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug');
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$locator->clearCache();
$this->getCache('index')->clear();
$this->getCache('object')->clear();
$this->getCache('render')->clear();
$this->index = null;
return $this;
}
/**
* @param string|null $key
* @return string
*/
public function getStorageFolder(string $key = null): string
{
return $this->getStorage()->getStoragePath($key);
}
/**
* @param string|null $key
* @return string
*/
public function getMediaFolder(string $key = null): string
{
return $this->getStorage()->getMediaPath($key);
}
/**
* @return FlexStorageInterface
*/
public function getStorage(): FlexStorageInterface
{
if (null === $this->storage) {
$this->storage = $this->createStorage();
}
return $this->storage;
}
/**
* @param array $data
* @param string $key
* @param bool $validate
* @return FlexObjectInterface
*/
public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface
{
/** @var string|FlexObjectInterface $className */
$className = $this->objectClassName ?: $this->getObjectClass();
return new $className($data, $key, $this, $validate);
}
/**
* @param array $entries
* @param string $keyField
* @return FlexCollectionInterface
*/
public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface
{
/** @var string|FlexCollectionInterface $className */
$className = $this->collectionClassName ?: $this->getCollectionClass();
return $className::createFromArray($entries, $this, $keyField);
}
/**
* @param array $entries
* @param string $keyField
* @return FlexIndexInterface
*/
public function createIndex(array $entries, string $keyField = null): FlexIndexInterface
{
/** @var string|FlexIndexInterface $className */
$className = $this->indexClassName ?: $this->getIndexClass();
return $className::createFromArray($entries, $this, $keyField);
}
/**
* @return string
*/
public function getObjectClass(): string
{
if (!$this->objectClassName) {
$this->objectClassName = $this->getConfig('data.object', 'Grav\\Framework\\Flex\\FlexObject');
}
return $this->objectClassName;
}
/**
* @return string
*/
public function getCollectionClass(): string
{
if (!$this->collectionClassName) {
$this->collectionClassName = $this->getConfig('data.collection', 'Grav\\Framework\\Flex\\FlexCollection');
}
return $this->collectionClassName;
}
/**
* @return string
*/
public function getIndexClass(): string
{
if (!$this->indexClassName) {
$this->indexClassName = $this->getConfig('data.index', 'Grav\\Framework\\Flex\\FlexIndex');
}
return $this->indexClassName;
}
/**
* @param array $entries
* @param string $keyField
* @return FlexCollectionInterface
*/
public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface
{
return $this->createCollection($this->loadObjects($entries), $keyField);
}
/**
* @param array $entries
* @return FlexObjectInterface[]
* @internal
*/
public function loadObjects(array $entries): array
{
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->startTimer('flex-objects', sprintf('Flex: Initializing %d %s', \count($entries), $this->type));
$storage = $this->getStorage();
$cache = $this->getCache('object');
// Get storage keys for the objects.
$keys = [];
$rows = [];
foreach ($entries as $key => $value) {
$k = $value['storage_key'];
$keys[$k] = $key;
$rows[$k] = null;
}
// Fetch rows from the cache.
try {
$rows = $cache->getMultiple(array_keys($rows));
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
}
// Read missing rows from the storage.
$updated = [];
$rows = $storage->readRows($rows, $updated);
// Store updated rows to the cache.
if ($updated) {
try {
if (!$cache instanceof MemoryCache) {
$debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug');
}
$cache->setMultiple($updated);
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
// TODO: log about the issue.
}
}
// Create objects from the rows.
$list = [];
foreach ($rows as $storageKey => $row) {
if ($row === null) {
$debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
continue;
}
if (isset($row['__error'])) {
$message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__error']);
$debugger->addException(new \RuntimeException($message));
$debugger->addMessage($message, 'error');
continue;
}
$usedKey = $keys[$storageKey];
$row += [
'storage_key' => $storageKey,
'storage_timestamp' => $entries[$usedKey]['storage_timestamp'],
];
$key = $entries[$usedKey]['key'] ?? $usedKey;
$object = $this->createObject($row, $key, false);
$list[$usedKey] = $object;
}
$debugger->stopTimer('flex-objects');
return $list;
}
/**
* @param string $type_view
* @param string $context
* @return Blueprint
*/
protected function getBlueprintInternal(string $type_view = '', string $context = '')
{
if (!isset($this->blueprints[$type_view])) {
if (!file_exists($this->blueprint_file)) {
throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type));
}
$parts = explode('.', rtrim($type_view, '.'), 2);
$type = array_shift($parts);
$view = array_shift($parts) ?: '';
$blueprint = new Blueprint($this->getBlueprintFile($view));
if ($context) {
$blueprint->setContext($context);
}
$blueprint->load($type ?: null);
if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) {
$blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load();
$blueprint->extend($blueprintBase, true);
}
$this->blueprints[$type_view] = $blueprint;
}
return $this->blueprints[$type_view];
}
/**
* @return FlexStorageInterface
*/
protected function createStorage(): FlexStorageInterface
{
$this->collection = $this->createCollection([]);
$storage = $this->getConfig('data.storage');
if (!\is_array($storage)) {
$storage = ['options' => ['folder' => $storage]];
}
$className = $storage['class'] ?? SimpleStorage::class;
$options = $storage['options'] ?? [];
return new $className($options);
}
/**
* @return FlexIndexInterface
*/
protected function loadIndex(): FlexIndexInterface
{
static $i = 0;
$index = $this->index;
if (null === $index) {
$i++; $j = $i;
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index");
$storage = $this->getStorage();
$cache = $this->getCache('index');
try {
$keys = $cache->get('__keys');
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
$keys = null;
}
if (null === $keys) {
/** @var string|FlexIndexInterface $className */
$className = $this->getIndexClass();
$keys = $className::loadEntriesFromStorage($storage);
if (!$cache instanceof MemoryCache) {
$debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)),
'debug');
}
try {
$cache->set('__keys', $keys);
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
// TODO: log about the issue.
}
}
// We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop.
$this->index = $this->createIndex($keys);
/** @var FlexCollectionInterface $collection */
$collection = $this->index->orderBy($this->getConfig('data.ordering', []));
$this->index = $index = $collection->getIndex();
$debugger->stopTimer('flex-keys-' . $this->type . $j);
}
return $index;
}
}
+344
View File
@@ -0,0 +1,344 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\Utils;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Form\Traits\FormTrait;
use Grav\Framework\Route\Route;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper;
/**
* Class FlexForm
* @package Grav\Framework\Flex
*/
class FlexForm implements FlexFormInterface
{
use FormTrait {
FormTrait::doSerialize as doTraitSerialize;
FormTrait::doUnserialize as doTraitUnserialize;
}
/** @var array|null */
private $form;
/** @var FlexObjectInterface */
private $object;
/**
* FlexForm constructor.
* @param string $name
* @param FlexObjectInterface $object
* @param array|null $form
*/
public function __construct(string $name, FlexObjectInterface $object, array $form = null)
{
$this->name = $name;
$this->form = $form;
$uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getFlexType()}:new";
$this->setObject($object);
$this->setId($this->getName());
$this->setUniqueId(md5($uniqueId));
$this->messages = [];
$this->submitted = false;
$flash = $this->getFlash();
if ($flash->exists()) {
$data = $flash->getData();
$includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
$this->data = $data ? new Data($data, $this->getBlueprint()) : null;
$this->files = $flash->getFilesByFields($includeOriginal);
} else {
$this->data = null;
$this->files = [];
}
}
/**
* @return string
*/
public function getName(): string
{
$object = $this->getObject();
$name = $this->name ?: 'object';
return "flex-{$object->getFlexType()}-{$name}";
}
/**
* @return Data|FlexObjectInterface|object
*/
public function getData()
{
return $this->data ?? $this->getObject();
}
/**
* Get a value from the form.
*
* Note: Used in form fields.
*
* @param string $name
* @return mixed
*/
public function getValue(string $name)
{
// Attempt to get value from the form data.
$value = $this->data ? $this->data[$name] : null;
// Return the form data or fall back to the object property.
return $value ?? $this->getObject()->getFormValue($name);
}
public function getDefaultValue(string $name)
{
return $this->object->getDefaultValue($name);
}
/**
* @return array
*/
public function getDefaultValues(): array
{
return $this->object->getDefaultValues();
}
/**
* @return string
*/
public function getFlexType(): string
{
return $this->object->getFlexType();
}
/**
* @return FlexObjectInterface
*/
public function getObject(): FlexObjectInterface
{
return $this->object;
}
public function updateObject(): FlexObjectInterface
{
$data = $this->data instanceof Data ? $this->data->toArray() : [];
$files = $this->files;
return $this->getObject()->update($data, $files);
}
/**
* @return Blueprint
*/
public function getBlueprint(): Blueprint
{
if (null === $this->blueprint) {
try {
$blueprint = $this->getObject()->getBlueprint(Utils::isAdminPlugin() ? '' : $this->name);
if ($this->form) {
// We have field overrides available.
$blueprint->extend(['form' => $this->form], true);
$blueprint->init();
}
} catch (\RuntimeException $e) {
if (!isset($this->form['fields'])) {
throw $e;
}
// Blueprint is not defined, but we have custom form fields available.
$blueprint = new Blueprint(null, ['form' => $this->form]);
$blueprint->load();
$blueprint->setScope('object');
$blueprint->init();
}
$this->blueprint = $blueprint;
}
return $this->blueprint;
}
/**
* @return Route|null
*/
public function getFileUploadAjaxRoute(): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
}
return $object->route('/edit.json/task:media.upload');
}
/**
* @param string $field
* @param string $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
}
return $object->route('/edit.json/task:media.delete');
}
public function getMediaTaskRoute(array $params = [], $extension = null): string
{
$grav = Grav::instance();
/** @var Flex $flex */
$flex = $grav['flex_objects'];
if (method_exists($flex, 'adminRoute')) {
return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json');
}
return '';
}
/**
* Implements \Serializable::unserialize().
*
* @param string $data
*/
public function unserialize($data): void
{
$data = unserialize($data, ['allowed_classes' => [FlexObject::class]]);
$this->doUnserialize($data);
}
public function __get($name)
{
$method = "get{$name}";
if (method_exists($this, $method)) {
return $this->{$method}();
}
$form = $this->getBlueprint()->form();
return $form[$name] ?? null;
}
public function __set($name, $value)
{
$method = "set{$name}";
if (method_exists($this, $method)) {
$this->{$method}($value);
}
}
public function __isset($name)
{
$method = "get{$name}";
if (method_exists($this, $method)) {
return true;
}
$form = $this->getBlueprint()->form();
return isset($form[$name]);
}
public function __unset($name)
{
}
/**
* Note: this method clones the object.
*
* @param FlexObjectInterface $object
* @return $this
*/
protected function setObject(FlexObjectInterface $object): self
{
$this->object = clone $object;
return $this;
}
/**
* @param string $layout
* @return Template|TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
protected function getTemplate($layout)
{
$grav = Grav::instance();
/** @var Twig $twig */
$twig = $grav['twig'];
return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
"flex-objects/layouts/_default/form/{$layout}.html.twig",
"forms/{$layout}/form.html.twig",
'forms/default/form.html.twig'
]
);
}
/**
* @param array $data
* @param array $files
* @throws \Exception
*/
protected function doSubmit(array $data, array $files)
{
/** @var FlexObject $object */
$object = clone $this->getObject();
$object->update($data, $files);
$object->save();
$this->setObject($object);
$this->reset();
}
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'object' => $this->object,
];
}
protected function doUnserialize(array $data): void
{
$this->doTraitUnserialize($data);
$this->object = $data['object'];
}
/**
* Filter validated data.
*
* @param \ArrayAccess $data
*/
protected function filterData(\ArrayAccess $data): void
{
if ($data instanceof Data) {
$data->filter(true, true);
}
}
}
@@ -0,0 +1,704 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Common\Debugger;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Session;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Collection\CollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
use Grav\Framework\Object\Interfaces\ObjectInterface;
use Grav\Framework\Object\ObjectIndex;
use Monolog\Logger;
use Psr\SimpleCache\InvalidArgumentException;
class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface
{
/** @var FlexDirectory */
private $_flexDirectory;
/** @var string */
private $_keyField;
/** @var array */
private $_indexKeys;
/**
* @param FlexDirectory $directory
* @return static
*/
public static function createFromStorage(FlexDirectory $directory)
{
return static::createFromArray(static::loadEntriesFromStorage($directory->getStorage()), $directory);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::createFromArray()
*/
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
{
$instance = new static($entries, $directory);
$instance->setKeyField($keyField);
return $instance;
}
/**
* @param FlexStorageInterface $storage
* @return array
*/
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
{
return $storage->getExistingKeys();
}
/**
* Initializes a new FlexIndex.
*
* @param array $entries
* @param FlexDirectory|null $directory
*/
public function __construct(array $entries = [], FlexDirectory $directory = null)
{
parent::__construct($entries);
$this->_flexDirectory = $directory;
$this->setKeyField(null);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::search()
*/
public function search(string $search, $properties = null, array $options = null)
{
return $this->__call('search', [$search, $properties, $options]);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::sort()
*/
public function sort(array $orderings)
{
return $this->orderBy($orderings);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::filterBy()
*/
public function filterBy(array $filters)
{
return $this->__call('filterBy', [$filters]);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexType()
*/
public function getFlexType(): string
{
return $this->_flexDirectory->getFlexType();
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexDirectory()
*/
public function getFlexDirectory(): FlexDirectory
{
return $this->_flexDirectory;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getTimestamp()
*/
public function getTimestamp(): int
{
$timestamps = $this->getTimestamps();
return $timestamps ? max($timestamps) : time();
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getCacheKey()
*/
public function getCacheKey(): string
{
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getCacheChecksum()
*/
public function getCacheChecksum(): string
{
return sha1($this->getCacheKey() . json_encode($this->getTimestamps()));
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getTimestamps()
*/
public function getTimestamps(): array
{
return $this->getIndexMap('storage_timestamp');
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getStorageKeys()
*/
public function getStorageKeys(): array
{
return $this->getIndexMap('storage_key');
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getFlexKeys()
*/
public function getFlexKeys(): array
{
// Get storage keys for the objects.
$keys = [];
$type = $this->_flexDirectory->getFlexType() . '.obj:';
foreach ($this->getEntries() as $key => $value) {
$keys[$key] = $value['flex_key'] ?? $type . $value['storage_key'];
}
return $keys;
}
/**
* {@inheritdoc}
* @see FlexIndexInterface::withKeyField()
*/
public function withKeyField(string $keyField = null)
{
$keyField = $keyField ?: 'key';
if ($keyField === $this->getKeyField()) {
return $this;
}
$type = $keyField === 'flex_key' ? $this->_flexDirectory->getFlexType() . '.obj:' : '';
$entries = [];
foreach ($this->getEntries() as $key => $value) {
if (!isset($value['key'])) {
$value['key'] = $key;
}
if (isset($value[$keyField])) {
$entries[$value[$keyField]] = $value;
} elseif ($keyField === 'flex_key') {
$entries[$type . $value['storage_key']] = $value;
}
}
return $this->createFrom($entries, $keyField);
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getIndex()
*/
public function getIndex()
{
return $this;
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::render()
*/
public function render(string $layout = null, array $context = [])
{
return $this->__call('render', [$layout, $context]);
}
/**
* @param bool $prefix
* @return string
* @deprecated 1.6 Use `->getFlexType()` instead.
*/
public function getType($prefix = false)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
$type = $prefix ? $this->getTypePrefix() : '';
return $type . $this->getFlexType();
}
/**
* {@inheritdoc}
* @see FlexIndexInterface::getFlexKeys()
*/
public function getIndexMap(string $indexKey = null)
{
if (null === $indexKey) {
return $this->getEntries();
}
// Get storage keys for the objects.
$index = [];
foreach ($this->getEntries() as $key => $value) {
$index[$key] = $value[$indexKey] ?? null;
}
return $index;
}
/**
* @return array
*/
public function getMetaData(string $key): array
{
return $this->getEntries()[$key] ?? [];
}
/**
* @return string
*/
public function getKeyField() : string
{
return $this->_keyField ?? 'storage_key';
}
/**
* @param string|null $namespace
* @return CacheInterface
*/
public function getCache(string $namespace = null)
{
return $this->_flexDirectory->getCache($namespace);
}
/**
* @param array $orderings
* @return FlexIndex|FlexCollection
*/
public function orderBy(array $orderings)
{
if (!$orderings || !$this->count()) {
return $this;
}
// Check if ordering needs to load the objects.
if (array_diff_key($orderings, $this->getIndexKeys())) {
return $this->__call('orderBy', [$orderings]);
}
// Ordering can be done by using index only.
$previous = null;
foreach (array_reverse($orderings) as $field => $ordering) {
$field = (string)$field;
if ($this->getKeyField() === $field) {
$keys = $this->getKeys();
$search = array_combine($keys, $keys) ?: [];
} elseif ($field === 'flex_key') {
$search = $this->getFlexKeys();
} else {
$search = $this->getIndexMap($field);
}
// Update current search to match the previous ordering.
if (null !== $previous) {
$search = array_replace($previous, $search);
}
// Order by current field.
if ($ordering === 'DESC') {
arsort($search, SORT_NATURAL);
} else {
asort($search, SORT_NATURAL);
}
$previous = $search;
}
return $this->createFrom(array_replace($previous, $this->getEntries()));
}
/**
* {@inheritDoc}
*/
public function call($method, array $arguments = [])
{
return $this->__call('call', [$method, $arguments]);
}
public function __call($name, $arguments)
{
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
/** @var FlexCollection $className */
$className = $this->_flexDirectory->getCollectionClass();
$cachedMethods = $className::getCachedMethods();
$flexType = $this->getFlexType();
if (!empty($cachedMethods[$name])) {
$type = $cachedMethods[$name];
if ($type === 'session') {
/** @var Session $session */
$session = Grav::instance()['session'];
$cacheKey = $session->getId() . $session->user->username;
} else {
$cacheKey = '';
}
$key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey());
$cache = $this->getCache('object');
try {
$result = $cache->get($key);
// Make sure the keys aren't changed if the returned type is the same index type.
if ($result instanceof self && $flexType === $result->getFlexType()) {
$result = $result->withKeyField($this->getKeyField());
}
} catch (InvalidArgumentException $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
}
if (!isset($result)) {
$collection = $this->loadCollection();
$result = $collection->{$name}(...$arguments);
try {
// If flex collection is returned, convert it back to flex index.
if ($result instanceof FlexCollection) {
$cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField());
} else {
$cached = $result;
}
$cache->set($key, $cached);
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
// TODO: log error.
}
}
} else {
$collection = $this->loadCollection();
$result = $collection->{$name}(...$arguments);
if (!isset($cachedMethods[$name])) {
$class = \get_class($collection);
$debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug');
}
}
return $result;
}
/**
* @return string
*/
public function serialize()
{
return serialize(['type' => $this->getFlexType(), 'entries' => $this->getEntries()]);
}
/**
* @param string $serialized
*/
public function unserialize($serialized)
{
$data = unserialize($serialized, ['allowed_classes' => false]);
$this->_flexDirectory = Grav::instance()['flex_objects']->getDirectory($data['type']);
$this->setEntries($data['entries']);
}
/**
* @param array $entries
* @param string $keyField
* @return static
*/
protected function createFrom(array $entries, string $keyField = null)
{
$index = new static($entries, $this->_flexDirectory);
$index->setKeyField($keyField ?? $this->_keyField);
return $index;
}
/**
* @param string|null $keyField
*/
protected function setKeyField(string $keyField = null)
{
$this->_keyField = $keyField ?? 'storage_key';
}
protected function getIndexKeys()
{
if (null === $this->_indexKeys) {
$entries = $this->getEntries();
$first = reset($entries);
if ($first) {
$keys = array_keys($first);
$keys = array_combine($keys, $keys) ?: [];
} else {
$keys = [];
}
$this->setIndexKeys($keys);
}
return $this->_indexKeys;
}
/**
* @param array $indexKeys
*/
protected function setIndexKeys(array $indexKeys)
{
// Add defaults.
$indexKeys += [
'key' => 'key',
'storage_key' => 'storage_key',
'storage_timestamp' => 'storage_timestamp',
'flex_key' => 'flex_key'
];
$this->_indexKeys = $indexKeys;
}
/**
* @return string
*/
protected function getTypePrefix()
{
return 'i.';
}
/**
* @param string $key
* @param mixed $value
* @return ObjectInterface|null
*/
protected function loadElement($key, $value): ?ObjectInterface
{
$objects = $this->_flexDirectory->loadObjects([$key => $value]);
return $objects ? reset($objects): null;
}
/**
* @param array|null $entries
* @return ObjectInterface[]
*/
protected function loadElements(array $entries = null): array
{
return $this->_flexDirectory->loadObjects($entries ?? $this->getEntries());
}
/**
* @param array|null $entries
* @return ObjectCollectionInterface
*/
protected function loadCollection(array $entries = null): CollectionInterface
{
return $this->_flexDirectory->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
}
/**
* @param mixed $value
* @return bool
*/
protected function isAllowedElement($value): bool
{
return $value instanceof FlexObject;
}
/**
* @param FlexObjectInterface $object
* @return mixed
*/
protected function getElementMeta($object)
{
return $object->getTimestamp();
}
/**
* @param FlexStorageInterface $storage
* @param array $index Saved index
* @param array $entries Updated index
* @return array Compiled list of entries
*/
protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries): array
{
// Calculate removed objects.
$removed = array_diff_key($index, $entries);
// First get rid of all removed objects.
if ($removed) {
$index = array_diff_key($index, $removed);
}
if ($entries) {
// Calculate difference between saved index and current data.
foreach ($index as $key => $entry) {
$storage_key = $entry['storage_key'] ?? null;
if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) {
// Entry is up to date, no update needed.
unset($entries[$storage_key]);
}
}
if (empty($entries) && empty($removed)) {
// No objects were added, updated or removed.
return $index;
}
} elseif (!$removed) {
// There are no objects and nothing was removed.
return [];
}
// Index should be updated, lock the index file for saving.
$indexFile = static::getIndexFile($storage);
$indexFile->lock();
// Read all the data rows into an array.
$keys = array_fill_keys(array_keys($entries), null);
$rows = $storage->readRows($keys);
$keyField = $storage->getKeyField();
// Go through all the updated objects and refresh their index data.
$updated = $added = [];
foreach ($rows as $key => $row) {
if (null !== $row) {
$entry = ['key' => $key] + $entries[$key];
if ($keyField !== 'storage_key' && isset($row[$keyField])) {
$entry['key'] = $row[$keyField];
}
static::updateIndexData($entry, $row);
if (isset($row['__error'])) {
$entry['__error'] = true;
static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error'])));
}
if (isset($index[$key])) {
// Update object in the index.
$updated[$key] = $entry;
} else {
// Add object into the index.
$added[$key] = $entry;
}
// Either way, update the entry.
$index[$key] = $entry;
} elseif (isset($index[$key])) {
// Remove object from the index.
$removed[$key] = $index[$key];
unset($index[$key]);
}
}
// Sort the index before saving it.
ksort($index, SORT_NATURAL);
static::onChanges($index, $added, $updated, $removed);
$indexFile->save(['count' => \count($index), 'index' => $index]);
$indexFile->unlock();
return $index;
}
protected static function updateIndexData(array &$entry, array $data)
{
}
protected static function loadEntriesFromIndex(FlexStorageInterface $storage)
{
$indexFile = static::getIndexFile($storage);
$data = [];
try {
$data = (array)$indexFile->content();
} catch (\Exception $e) {
$e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e);
static::onException($e);
}
return $data['index'] ?? [];
}
protected static function getIndexFile(FlexStorageInterface $storage)
{
// Load saved index file.
$grav = Grav::instance();
$locator = $grav['locator'];
$filename = $locator->findResource($storage->getStoragePath() . '/index.yaml', true, true);
return CompiledYamlFile::instance($filename);
}
protected static function onException(\Exception $e)
{
$grav = Grav::instance();
/** @var Logger $logger */
$logger = $grav['log'];
$logger->addAlert($e->getMessage());
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->addException($e);
$debugger->addMessage($e, 'error');
}
protected static function onChanges(array $entries, array $added, array $updated, array $removed)
{
$message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed));
$grav = Grav::instance();
/** @var Logger $logger */
$logger = $grav['log'];
$logger->addDebug($message);
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->addMessage($message, 'debug');
}
public function __debugInfo()
{
return [
'type:private' => $this->getFlexType(),
'key:private' => $this->getKey(),
'entries_key:private' => $this->getKeyField(),
'entries:private' => $this->getEntries()
];
}
}
@@ -0,0 +1,877 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Common\Data\Blueprint;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\Utils;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Grav\Framework\Object\Access\NestedArrayAccessTrait;
use Grav\Framework\Object\Access\NestedPropertyTrait;
use Grav\Framework\Object\Access\OverloadedPropertyTrait;
use Grav\Framework\Object\Base\ObjectTrait;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectInterface;
use Grav\Framework\Object\Property\LazyPropertyTrait;
use Psr\SimpleCache\InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper;
/**
* Class FlexObject
* @package Grav\Framework\Flex
*/
class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{
use ObjectTrait;
use LazyPropertyTrait {
LazyPropertyTrait::__construct as private objectConstruct;
}
use NestedPropertyTrait;
use OverloadedPropertyTrait;
use NestedArrayAccessTrait;
use FlexAuthorizeTrait;
/** @var FlexDirectory */
private $_flexDirectory;
/** @var FlexFormInterface[] */
private $_forms = [];
/** @var array */
private $_storage;
/** @var array */
protected $_changes;
/**
* @return array
*/
public static function getCachedMethods(): array
{
return [
'getTypePrefix' => true,
'getType' => true,
'getFlexType' => true,
'getFlexDirectory' => true,
'getCacheKey' => true,
'getCacheChecksum' => true,
'getTimestamp' => true,
'value' => true,
'exists' => true,
'hasProperty' => true,
'getProperty' => true,
// FlexAclTrait
'isAuthorized' => 'session',
];
}
public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false)
{
$instance = new static($elements, $storage['key'], $directory, $validate);
$instance->setStorage($storage);
return $instance;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::__construct()
*/
public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
{
$this->_flexDirectory = $directory;
if ($validate) {
$blueprint = $this->getFlexDirectory()->getBlueprint();
$blueprint->validate($elements);
$elements = $blueprint->filter($elements);
}
$this->filterElements($elements);
$this->objectConstruct($elements, $key);
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getFlexType()
*/
public function getFlexType(): string
{
return $this->_flexDirectory->getFlexType();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getFlexDirectory()
*/
public function getFlexDirectory(): FlexDirectory
{
return $this->_flexDirectory;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getTimestamp()
*/
public function getTimestamp(): int
{
return $this->_storage['storage_timestamp'] ?? 0;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getCacheKey()
*/
public function getCacheKey(): string
{
return $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getStorageKey();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getCacheChecksum()
*/
public function getCacheChecksum(): string
{
return (string)$this->getTimestamp();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::search()
*/
public function search(string $search, $properties = null, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []);
if (!$properties) {
foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
$weight = 0;
foreach ((array)$properties as $property) {
$weight += $this->searchNestedProperty($property, $search, $options);
}
return $weight > 0 ? min($weight, 1) : 0;
}
/**
* {@inheritdoc}
* @see ObjectInterface::getFlexKey()
*/
public function getKey()
{
return $this->_key ?: $this->getFlexType() . '@@' . spl_object_hash($this);
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getFlexKey()
*/
public function getFlexKey(): string
{
return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getStorageKey()
*/
public function getStorageKey(): string
{
return $this->_storage['storage_key'] ?? $this->getTypePrefix() . $this->getFlexType() . '@@' . spl_object_hash($this);
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getMetaData()
*/
public function getMetaData(): array
{
return $this->getStorage();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::exists()
*/
public function exists(): bool
{
$key = $this->getStorageKey();
return $key && $this->getFlexDirectory()->getStorage()->hasKey($key);
}
/**
* @param string $property
* @param string $search
* @param array|null $options
* @return float
*/
public function searchProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$value = $this->getProperty($property);
return $this->searchValue($property, $value, $search, $options);
}
/**
* @param string $property
* @param string $search
* @param array|null $options
* @return float
*/
public function searchNestedProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$value = $this->getNestedProperty($property);
return $this->searchValue($property, $value, $search, $options);
}
/**
* @param string $name
* @param mixed $value
* @param string $search
* @param array|null $options
* @return float
*/
protected function searchValue(string $name, $value, string $search, array $options = null): float
{
$search = trim($search);
if ($search === '') {
return 0;
}
if (!\is_string($value) || $value === '') {
return 0;
}
$tested = false;
if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $options['case_sensitive'] ?? false)) {
return (float)$options['starts_with'];
}
if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $options['case_sensitive'] ?? false)) {
return (float)$options['ends_with'];
}
if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) {
return (float)($options['contains'] ?? 1);
}
return 0;
}
/**
* Get any changes based on data sent to update
*
* @return array
*/
public function getChanges(): array
{
return $this->_changes ?? [];
}
/**
* @return string
*/
protected function getTypePrefix(): string
{
return 'o.';
}
/**
* @param bool $prefix
* @return string
* @deprecated 1.6 Use `->getFlexType()` instead.
*/
public function getType($prefix = false)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
$type = $prefix ? $this->getTypePrefix() : '';
return $type . $this->getFlexType();
}
/**
* Alias of getBlueprint()
*
* @return Blueprint
* @deprecated 1.6 Admin compatibility
*/
public function blueprints()
{
return $this->getBlueprint();
}
/**
* @param string|null $namespace
* @return CacheInterface
*/
public function getCache(string $namespace = null)
{
return $this->_flexDirectory->getCache($namespace);
}
/**
* @param string|null $key
* @return $this
*/
public function setStorageKey($key = null)
{
$this->_storage['storage_key'] = $key;
return $this;
}
/**
* @param int $timestamp
* @return $this
*/
public function setTimestamp($timestamp = null)
{
$this->_storage['storage_timestamp'] = $timestamp ?? time();
return $this;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::render()
*/
public function render(string $layout = null, array $context = [])
{
if (null === $layout) {
$layout = 'default';
}
$type = $this->getFlexType();
$grav = Grav::instance();
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')');
$cache = $key = null;
foreach ($context as $value) {
if (!\is_scalar($value)) {
$key = false;
}
}
if ($key !== false) {
$key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
$cache = $this->getCache('render');
}
try {
$data = $cache && $key ? $cache->get($key) : null;
$block = $data ? HtmlBlock::fromArray($data) : null;
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
$block = null;
} catch (\InvalidArgumentException $e) {
$debugger->addException($e);
$block = null;
}
$checksum = $this->getCacheChecksum();
if ($block && $checksum !== $block->getChecksum()) {
$block = null;
}
if (!$block) {
$block = HtmlBlock::create($key ?: null);
$block->setChecksum($checksum);
if ($key === false) {
$block->disableCache();
}
$grav->fireEvent('onFlexObjectRender', new Event([
'object' => $this,
'layout' => &$layout,
'context' => &$context
]));
$output = $this->getTemplate($layout)->render(
['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'object' => $this, 'layout' => $layout] + $context
);
if ($debugger->enabled()) {
$name = $this->getKey() . ' (' . $type . ')';
$output = "\n<!–– START {$name} object ––>\n{$output}\n<!–– END {$name} object ––>\n";
}
$block->setContent($output);
try {
$cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
}
}
$debugger->stopTimer('flex-object-' . $debugKey);
return $block;
}
/**
* @return array
*/
public function jsonSerialize()
{
return $this->getElements();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::prepareStorage()
*/
public function prepareStorage(): array
{
return $this->getElements();
}
/**
* @param string $name
* @return $this
*/
public function triggerEvent($name)
{
return $this;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::update()
*/
public function update(array $data, array $files = [])
{
if ($data) {
$blueprint = $this->getBlueprint();
// Process updated data through the object filters.
$this->filterElements($data);
// Get currently stored data.
$elements = $this->getElements();
// Merge existing object to the test data to be validated.
$test = $blueprint->mergeData($elements, $data);
// Validate and filter elements and throw an error if any issues were found.
$blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]);
$data = $blueprint->filter($data, false, true);
// Finally update the object.
foreach ($blueprint->flattenData($data) as $key => $value) {
if ($value === null) {
$this->unsetNestedProperty($key);
} else {
$this->setNestedProperty($key, $value);
}
}
// Store the changes
$this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
}
if ($files && method_exists($this, 'setUpdatedMedia')) {
$this->setUpdatedMedia($files);
}
return $this;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::create()
*/
public function create(string $key = null)
{
if ($key) {
$this->setStorageKey($key);
}
if ($this->exists()) {
throw new \RuntimeException('Cannot create new object (Already exists)');
}
return $this->save();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::save()
*/
public function save()
{
$this->triggerEvent('onBeforeSave');
$result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
$value = reset($result);
$storageKey = (string)key($result);
if ($value && $storageKey) {
$this->setStorageKey($storageKey);
if (!$this->hasKey()) {
$this->setKey($storageKey);
}
}
// FIXME: For some reason locator caching isn't cleared for the file, investigate!
$locator = Grav::instance()['locator'];
$locator->clearCache();
// Make sure that the object exists before continuing (just in case).
if (!$this->exists()) {
throw new \RuntimeException('Saving failed: Object does not exist!');
}
if (method_exists($this, 'saveUpdatedMedia')) {
$this->saveUpdatedMedia();
}
try {
$this->getFlexDirectory()->clearCache();
if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache();
}
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
// Caching failed, but we can ignore that for now.
}
$this->triggerEvent('onAfterSave');
return $this;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::delete()
*/
public function delete()
{
$this->triggerEvent('onBeforeDelete');
$this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
try {
$this->getFlexDirectory()->clearCache();
if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache();
}
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
// Caching failed, but we can ignore that for now.
}
$this->triggerEvent('onAfterDelete');
return $this;
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getBlueprint()
*/
public function getBlueprint(string $name = '')
{
return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name);
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getForm()
*/
public function getForm(string $name = '', array $form = null)
{
if (!isset($this->_forms[$name])) {
$this->_forms[$name] = $this->createFormObject($name, $form);
}
return $this->_forms[$name];
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getDefaultValue()
*/
public function getDefaultValue(string $name, string $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $name) ?: [];
$offset = array_shift($path) ?? '';
$current = $this->getDefaultValues();
if (!isset($current[$offset])) {
return null;
}
$current = $current[$offset];
while ($path) {
$offset = array_shift($path);
if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
$current = $current[$offset];
} elseif (\is_object($current) && isset($current->{$offset})) {
$current = $current->{$offset};
} else {
return null;
}
};
return $current;
}
/**
* @return array
*/
public function getDefaultValues(): array
{
return $this->getBlueprint()->getDefaults();
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::getFormValue()
*/
public function getFormValue(string $name, $default = null, string $separator = null)
{
if ($name === 'storage_key') {
return $this->getStorageKey();
}
if ($name === 'storage_timestamp') {
return $this->getTimestamp();
}
return $this->getNestedProperty($name, $default, $separator);
}
/**
* @param string $name
* @param mixed|null $default
* @param string|null $separator
* @return mixed
*
* @deprecated 1.6 Use ->getFormValue() method instead.
*/
public function value($name, $default = null, $separator = null)
{
return $this->getFormValue($name, $default, $separator);
}
/**
* Returns a string representation of this object.
*
* @return string
*/
public function __toString()
{
return $this->getFlexKey();
}
public function __debugInfo()
{
return [
'type:private' => $this->getFlexType(),
'key:private' => $this->getKey(),
'elements:private' => $this->getElements(),
'storage:private' => $this->getStorage()
];
}
/**
* @return array
*/
protected function doSerialize(): array
{
return [
'type' => $this->getFlexType(),
'key' => $this->getKey(),
'elements' => $this->getElements(),
'storage' => $this->getStorage()
];
}
/**
* @param array $serialized
*/
protected function doUnserialize(array $serialized): void
{
$type = $serialized['type'] ?? 'unknown';
if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) {
throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data");
}
$grav = Grav::instance();
/** @var Flex|null $flex */
$flex = $grav['flex_objects'] ?? null;
$directory = $flex ? $flex->getDirectory($type) : null;
if (!$directory) {
throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found");
}
$this->setFlexDirectory($directory);
$this->setStorage($serialized['storage']);
$this->setKey($serialized['key']);
$this->setElements($serialized['elements']);
}
/**
* @param FlexDirectory $directory
*/
public function setFlexDirectory(FlexDirectory $directory): void
{
$this->_flexDirectory = $directory;
}
/**
* @param array $storage
*/
protected function setStorage(array $storage) : void
{
$this->_storage = $storage;
}
/**
* @return array
*/
protected function getStorage() : array
{
return $this->_storage ?? [];
}
/**
* @param string $type
* @param string $property
* @return FlexCollectionInterface
*/
protected function getCollectionByProperty($type, $property)
{
$directory = $this->getRelatedDirectory($type);
$collection = $directory->getCollection();
$list = $this->getNestedProperty($property) ?: [];
/** @var FlexCollection $collection */
$collection = $collection->filter(function ($object) use ($list) { return \in_array($object->id, $list, true); });
return $collection;
}
/**
* @param string $type
* @return FlexDirectory
* @throws \RuntimeException
*/
protected function getRelatedDirectory($type): FlexDirectory
{
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
$directory = $flex->getDirectory($type);
if (!$directory) {
throw new \RuntimeException(ucfirst($type). ' directory does not exist!');
}
return $directory;
}
/**
* @param string $layout
* @return Template|TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
protected function getTemplate($layout)
{
$grav = Grav::instance();
/** @var Twig $twig */
$twig = $grav['twig'];
try {
return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig",
"flex-objects/layouts/_default/object/{$layout}.html.twig"
]
);
} catch (LoaderError $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']);
}
}
/**
* Filter data coming to constructor or $this->update() request.
*
* NOTE: The incoming data can be an arbitrary array so do not assume anything from its content.
*
* @param array $elements
*/
protected function filterElements(array &$elements): void
{
if (!empty($elements['storage_key'])) {
$this->_storage['storage_key'] = trim($elements['storage_key']);
}
if (!empty($elements['storage_timestamp'])) {
$this->_storage['storage_timestamp'] = (int)$elements['storage_timestamp'];
}
unset ($elements['storage_key'], $elements['storage_timestamp'], $elements['_post_entries_save']);
}
/**
* This methods allows you to override form objects in child classes.
*
* @param string $name Form name
* @param array|null $form Form fields
* @return FlexFormInterface
*/
protected function createFormObject(string $name, array $form = null)
{
return new FlexForm($name, $this, $form);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Common\User\Interfaces\UserInterface;
/**
* Defines authorization checks for Flex Objects.
*/
interface FlexAuthorizeInterface
{
/**
* Check if user is authorized to perform an action for the object.
*
* @param string $action One of: `create`, `read`, `update`, `delete`, `save`, `list`
* @param string|null $scope One of: `admin`, `site`
* @param UserInterface|null $user Optional user. Defaults to the current user.
*
* @return bool Returns `true` if user is authorized to perform action, `false` otherwise.
* @api
*/
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool;
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
use Grav\Framework\Flex\FlexDirectory;
/**
* Defines a collection of Flex Objects.
*
* @used-by \Grav\Framework\Flex\FlexCollection
* @since 1.6
*/
interface FlexCollectionInterface extends FlexCommonInterface, ObjectCollectionInterface, NestedObjectInterface
{
/**
* Creates a Flex Collection from an array.
*
* @used-by FlexDirectory::createCollection() Official method to create a Flex Collection.
*
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
* @param FlexDirectory $directory Flex Directory where all the objects belong into.
* @param string $keyField Key field used to index the collection.
*
* @return static Returns a new Flex Collection.
*/
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null);
/**
* Creates a new Flex Collection.
*
* @used-by FlexDirectory::createCollection() Official method to create Flex Collection.
*
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
* @param FlexDirectory $directory Flex Directory where all the objects belong into.
*
* @throws \InvalidArgumentException
*/
public function __construct(array $entries = [], FlexDirectory $directory = null);
/**
* Search a string from the collection.
*
* @param string $search Search string.
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
* @param array|null $options Search options, defaults to configured options.
*
* @return FlexCollectionInterface Returns a Flex Collection with only matching objects.
* @api
*/
public function search(string $search, $properties = null, array $options = null);
/**
* Sort the collection.
*
* @param array $orderings Pair of [property => 'ASC'|'DESC', ...].
*
* @return FlexCollectionInterface Returns a sorted version from the collection.
*/
public function sort(array $orderings);
/**
* Filter collection by filter array with keys and values.
*
* @param array $filters
* @return FlexCollectionInterface
*/
public function filterBy(array $filters);
/**
* Get timestamps from all the objects in the collection.
*
* This method can be used for example in caching.
*
* @return int[] Returns [key => timestamp, ...] pairs.
*/
public function getTimestamps(): array;
/**
* Get storage keys from all the objects in the collection.
*
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
*
* @return string[] Returns [key => storage_key, ...] pairs.
*/
public function getStorageKeys(): array;
/**
* Get Flex keys from all the objects in the collection.
*
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
*
* @return string[] Returns[key => flex_key, ...] pairs.
*/
public function getFlexKeys(): array;
/**
* Return new collection with a different key.
*
* @param string|null $keyField Switch key field of the collection.
*
* @return FlexCollectionInterface Returns a new Flex Collection with new key field.
* @api
*/
public function withKeyField(string $keyField = null);
/**
* Get Flex Index from the Flex Collection.
*
* @return FlexIndexInterface Returns a Flex Index from the current collection.
*/
public function getIndex();
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Interfaces\RenderInterface;
/**
* Defines common interface shared with both Flex Objects and Collections.
*
* @used-by \Grav\Framework\Flex\FlexObject
* @since 1.6
*/
interface FlexCommonInterface extends RenderInterface
{
/**
* Get Flex Type of the object / collection.
*
* @return string Returns Flex Type of the collection.
* @api
*/
public function getFlexType(): string;
/**
* Get Flex Directory for the object / collection.
*
* @return FlexDirectory Returns associated Flex Directory.
* @api
*/
public function getFlexDirectory(): FlexDirectory;
/**
* Get last updated timestamp for the object / collection.
*
* @return int Returns Unix timestamp.
* @api
*/
public function getTimestamp(): int;
/**
* Get a cache key which is used for caching the object / collection.
*
* @return string Returns cache key.
*/
public function getCacheKey(): string;
/**
* Get cache checksum for the object / collection.
*
* If checksum changes, cache gets invalided.
*
* @return string Returns cache checksum.
*/
public function getCacheChecksum(): string;
}
@@ -0,0 +1,54 @@
<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Route\Route;
/**
* Defines Forms for Flex Objects.
*
* @used-by \Grav\Framework\Flex\FlexForm
* @since 1.6
*/
interface FlexFormInterface extends \Serializable, FormInterface
{
/**
* Get object associated to the form.
*
* @return FlexObjectInterface Returns Flex Object associated to the form.
* @api
*/
public function getObject();
/**
* Get media task route.
*
* @return string Returns admin route for media tasks.
*/
public function getMediaTaskRoute(): string;
/**
* Get route for uploading files by AJAX.
*
* @return Route|null Returns Route object or null if file uploads are not enabled.
*/
public function getFileUploadAjaxRoute();
/**
* Get route for deleting files by AJAX.
*
* @param string $field Field where the file is associated into.
* @param string $filename Filename for the file.
*
* @return Route|null Returns Route object or null if file uploads are not enabled.
*/
public function getFileDeleteAjaxRoute($field, $filename);
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Framework\Flex\FlexDirectory;
/**
* Defines Indexes for Flex Objects.
*
* Flex indexes are similar to database indexes, they contain indexed fields which can be used to quickly look up or
* find the objects without loading them.
*
* @used-by \Grav\Framework\Flex\FlexIndex
* @since 1.6
*/
interface FlexIndexInterface extends FlexCollectionInterface
{
/**
* Helper method to create Flex Index.
*
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
*
* @param FlexDirectory $directory Flex directory.
*
* @return static Returns a new Flex Index.
*/
public static function createFromStorage(FlexDirectory $directory);
/**
* Method to load index from the object storage, usually filesystem.
*
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
*
* @param FlexStorageInterface $storage Flex Storage associated to the directory.
*
* @return array Returns a list of existing objects [storage_key => [storage_key => xxx, storage_timestamp => 123456, ...]]
*/
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array;
/**
* Return new collection with a different key.
*
* @param string|null $keyField Switch key field of the collection.
*
* @return FlexIndexInterface Returns a new Flex Collection with new key field.
* @api
*/
public function withKeyField(string $keyField = null);
/**
* @param string $indexKey
* @return array
*/
public function getIndexMap(string $indexKey = null);
}
@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
use Grav\Common\Data\Blueprint;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Flex\FlexDirectory;
use Psr\Http\Message\UploadedFileInterface;
/**
* Defines Flex Objects.
*
* @used-by \Grav\Framework\Flex\FlexObject
* @since 1.6
*/
interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface, \ArrayAccess
{
/**
* Construct a new Flex Object instance.
*
* @used-by FlexDirectory::createObject() Method to create Flex Object.
*
* @param array $elements Array of object properties.
* @param string $key Identifier key for the new object.
* @param FlexDirectory $directory Flex Directory the object belongs into.
* @param bool $validate True if the object should be validated against blueprint.
*
* @throws \InvalidArgumentException
*/
public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false);
/**
* Search a string from the object, returns weight between 0 and 1.
*
* Note: If you override this function, make sure you return value in range 0...1!
*
* @used-by FlexCollectionInterface::search() If you want to search a string from a Flex Collection.
*
* @param string $search Search string.
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
* @param array|null $options Search options, defaults to configured options.
*
* @return float Returns a weight between 0 and 1.
* @api
*/
public function search(string $search, $properties = null, array $options = null): float;
/**
* Get a unique key for the object.
*
* Flex Keys can be used without knowing the Directory the Object belongs into.
*
* @see Flex::getObject() If you want to get Flex Object from any Flex Directory.
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
*
* NOTE: Please do not override the method!
*
* @return string Returns Flex Key of the object.
* @api
*/
public function getFlexKey(): string;
/**
* Get an unique storage key (within the directory) which is used for figuring out the filename or database id.
*
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
* @see FlexDirectory::getCollection() If you want to get Flex Collection with selected keys from the Flex Directory.
*
* @return string Returns storage key of the Object.
* @api
*/
public function getStorageKey(): string;
/**
* Get index data associated to the object.
*
* @return array Returns metadata of the object.
*/
public function getMetaData(): array;
/**
* Returns true if the object exists in the storage.
*
* @return bool Returns `true` if the object exists, `false` otherwise.
* @api
*/
public function exists(): bool;
/**
* Prepare object for saving into the storage.
*
* @return array Returns an array of object properties containing only scalars and arrays.
*/
public function prepareStorage(): array;
/**
* Updates object in the memory.
*
* @see FlexObjectInterface::save() You need to save the object after calling this method.
*
* @param array $data Data containing updated properties with their values. To unset a value, use `null`.
* @param array|UploadedFileInterface[] $files List of uploaded files to be saved within the object.
*
* @return FlexObjectInterface
* @throws \RuntimeException
* @api
*/
public function update(array $data, array $files = []);
/**
* Create new object into the storage.
*
* @see FlexDirectory::createObject() If you want to create a new object instance.
* @see FlexObjectInterface::update() If you want to update properties of the object.
*
* @param string|null $key Optional new key. If key isn't given, random key will be associated to the object.
*
* @return FlexObjectInterface
* @throws \RuntimeException if object already exists.
* @api
*/
public function create(string $key = null);
/**
* Save object into the storage.
*
* @see FlexObjectInterface::update() If you want to update properties of the object.
*
* @return FlexObjectInterface
* @api
*/
public function save();
/**
* Delete object from the storage.
*
* @return FlexObjectInterface
* @api
*/
public function delete();
/**
* Returns the blueprint of the object.
*
* @see FlexObjectInterface::getForm()
* @used-by FlexForm::getBlueprint()
*
* @param string $name Name of the Blueprint form. Used to create customized forms for different use cases.
*
* @return Blueprint Returns a Blueprint.
*/
public function getBlueprint(string $name = '');
/**
* Returns a form instance for the object.
*
* @param string $name Name of the form. Can be used to create customized forms for different use cases.
* @param array|null $form Can be used to further customize the form.
*
* @return FlexFormInterface Returns a Form.
* @api
*/
public function getForm(string $name = '', array $form = null);
/**
* Returns default value suitable to be used in a form for the given property.
*
* @see FlexObjectInterface::getForm()
*
* @param string $name Property name.
* @param string $separator Optional nested property separator.
*
* @return mixed|null Returns default value of the field, null if there is no default value.
*/
public function getDefaultValue(string $name, string $separator = null);
/**
* Returns default values suitable to be used in a form for the given property.
*
* @see FlexObjectInterface::getForm()
*
* @return array Returns default values.
*/
public function getDefaultValues(): array;
/**
* Returns raw value suitable to be used in a form for the given property.
*
* @see FlexObjectInterface::getForm()
*
* @param string $name Property name.
* @param mixed $default Default value.
* @param string $separator Optional nested property separator.
*
* @return mixed Returns value of the field.
*/
public function getFormValue(string $name, $default = null, string $separator = null);
}
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Interfaces;
/**
* Defines Flex Storage layer.
*
* @since 1.6
*/
interface FlexStorageInterface
{
/**
* StorageInterface constructor.
* @param array $options
*/
public function __construct(array $options);
/**
* @return string
*/
public function getKeyField(): string;
/**
* Returns associated array of all existing storage keys with a timestamp.
*
* @return array Returns all existing keys as `[key => [storage_key => key, storage_timestamp => timestamp], ...]`.
*/
public function getExistingKeys(): array;
/**
* Check if the key exists in the storage.
*
* @param string $key Storage key of an object.
*
* @return bool Returns `true` if the key exists in the storage, `false` otherwise.
*/
public function hasKey(string $key): bool;
/**
* Check if the key exists in the storage.
*
* @param string[] $keys Storage key of an object.
*
* @return bool[] Returns keys with `true` if the key exists in the storage, `false` otherwise.
*/
public function hasKeys(array $keys): array;
/**
* Create new rows into the storage.
*
* New keys will be assigned when the objects are created.
*
* @param array $rows List of rows as `[row, ...]`.
*
* @return array Returns created rows as `[key => row, ...] pairs.
*/
public function createRows(array $rows): array;
/**
* Read rows from the storage.
*
* If you pass object or array as value, that value will be used to save I/O.
*
* @param array $rows Array of `[key => row, ...]` pairs.
* @param array $fetched Optional reference to store only fetched items.
*
* @return array Returns rows. Note that non-existing rows will have `null` as their value.
*/
public function readRows(array $rows, array &$fetched = null): array;
/**
* Update existing rows in the storage.
*
* @param array $rows Array of `[key => row, ...]` pairs.
*
* @return array Returns updated rows. Note that non-existing rows will not be saved and have `null` as their value.
*/
public function updateRows(array $rows): array;
/**
* Delete rows from the storage.
*
* @param array $rows Array of `[key => row, ...]` pairs.
*
* @return array Returns deleted rows. Note that non-existing rows have `null` as their value.
*/
public function deleteRows(array $rows): array;
/**
* Replace rows regardless if they exist or not.
*
* All rows should have a specified key for replace to work properly.
*
* @param array $rows Array of `[key => row, ...]` pairs.
*
* @return array Returns both created and updated rows.
*/
public function replaceRows(array $rows): array;
/**
* @param string $src
* @param string $dst
*
* @return bool
*/
public function renameRow(string $src, string $dst): bool;
/**
* Get filesystem path for the collection or object storage.
*
* @param string|null $key Optional storage key.
*
* @return string Path in the filesystem. Can be URI.
*/
public function getStoragePath(string $key = null): string;
/**
* Get filesystem path for the collection or object media.
*
* @param string|null $key Optional storage key.
*
* @return string Path in the filesystem. Can be URI.
*/
public function getMediaPath(string $key = null): string;
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Storage;
use Grav\Common\File\CompiledJsonFile;
use Grav\Common\File\CompiledMarkdownFile;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\MarkdownFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
/**
* Class AbstractFilesystemStorage
* @package Grav\Framework\Flex\Storage
*/
abstract class AbstractFilesystemStorage implements FlexStorageInterface
{
/** @var FileFormatterInterface */
protected $dataFormatter;
/** @var string */
protected $keyField = 'storage_key';
/**
* {@inheritdoc}
* @see FlexStorageInterface::hasKey()
*/
public function hasKeys(array $keys): array
{
$list = [];
foreach ($keys as $key) {
$list[$key] = $this->hasKey((string)$key);
}
return $list;
}
/**
* @return string
*/
public function getKeyField(): string
{
return $this->keyField;
}
protected function initDataFormatter($formatter): void
{
// Initialize formatter.
if (!\is_array($formatter)) {
$formatter = ['class' => $formatter];
}
$formatterClassName = $formatter['class'] ?? JsonFormatter::class;
$formatterOptions = $formatter['options'] ?? [];
$this->dataFormatter = new $formatterClassName($formatterOptions);
}
/**
* @param string $filename
* @return null|string
*/
protected function detectDataFormatter(string $filename): ?string
{
if (preg_match('|(\.[a-z0-9]*)$|ui', $filename, $matches)) {
switch ($matches[1]) {
case '.json':
return JsonFormatter::class;
case '.yaml':
return YamlFormatter::class;
case '.md':
return MarkdownFormatter::class;
}
}
return null;
}
/**
* @param string $filename
* @return File
*/
protected function getFile(string $filename)
{
$filename = $this->resolvePath($filename);
switch ($this->dataFormatter->getDefaultFileExtension()) {
case '.json':
$file = CompiledJsonFile::instance($filename);
break;
case '.yaml':
$file = CompiledYamlFile::instance($filename);
break;
case '.md':
$file = CompiledMarkdownFile::instance($filename);
break;
default:
throw new RuntimeException('Unknown extension type ' . $this->dataFormatter->getDefaultFileExtension());
}
return $file;
}
/**
* @param string $path
* @return string
*/
protected function resolvePath(string $path): string
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if (!$locator->isStream($path)) {
return $path;
}
return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true));
}
/**
* Generates a random, unique key for the row.
*
* @return string
*/
protected function generateKey(): string
{
return substr(hash('sha256', random_bytes(32)), 0, 32);
}
/**
* Checks if a key is valid.
*
* @param string $key
* @return bool
*/
protected function validateKey(string $key): bool
{
return (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key);
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Storage;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
/**
* Class FileStorage
* @package Grav\Framework\Flex\Storage
*/
class FileStorage extends FolderStorage
{
/**
* {@inheritdoc}
* @see FlexStorageInterface::__construct()
*/
public function __construct(array $options)
{
$this->dataPattern = '{FOLDER}/{KEY}';
if (!isset($options['formatter']) && isset($options['pattern'])) {
$options['formatter'] = $this->detectDataFormatter($options['pattern']);
}
parent::__construct($options);
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getMediaPath()
*/
public function getMediaPath(string $key = null): string
{
return $key ? \dirname($this->getStoragePath($key)) . '/' . $key : $this->getStoragePath();
}
/**
* {@inheritdoc}
*/
protected function getKeyFromPath(string $path): string
{
return basename($path, $this->dataFormatter->getDefaultFileExtension());
}
/**
* {@inheritdoc}
*/
protected function buildIndex(): array
{
if (!file_exists($this->getStoragePath())) {
return [];
}
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
$iterator = new \FilesystemIterator($this->getStoragePath(), $flags);
$list = [];
/** @var \SplFileInfo $info */
foreach ($iterator as $filename => $info) {
if (!$info->isFile() || !($key = $this->getKeyFromPath($filename)) || strpos($info->getFilename(), '.') === 0) {
continue;
}
$list[$key] = [
'storage_key' => $key,
'storage_timestamp' => $info->getMTime()
];
}
ksort($list, SORT_NATURAL);
return $list;
}
}
@@ -0,0 +1,470 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Storage;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use RocketTheme\Toolbox\File\File;
use InvalidArgumentException;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Class FolderStorage
* @package Grav\Framework\Flex\Storage
*/
class FolderStorage extends AbstractFilesystemStorage
{
/** @var string */
protected $dataFolder;
/** @var string */
protected $dataPattern = '{FOLDER}/{KEY}/item';
/** @var bool */
protected $prefixed;
/** @var bool */
protected $indexed;
/**
* {@inheritdoc}
*/
public function __construct(array $options)
{
if (!isset($options['folder'])) {
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
}
$this->initDataFormatter($options['formatter'] ?? []);
$this->initOptions($options);
// Make sure that the data folder exists.
$folder = $this->resolvePath($this->dataFolder);
if (!file_exists($folder)) {
try {
Folder::create($folder);
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage()));
}
}
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getExistingKeys()
*/
public function getExistingKeys(): array
{
return $this->buildIndex();
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::hasKey()
*/
public function hasKey(string $key): bool
{
return $key && strpos($key, '@@') === false && file_exists($this->getPathFromKey($key));
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::createRows()
*/
public function createRows(array $rows): array
{
$list = [];
foreach ($rows as $key => $row) {
// Create new file and save it.
$key = $this->getNewKey();
$path = $this->getPathFromKey($key);
$file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row);
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::readRows()
*/
public function readRows(array $rows, array &$fetched = null): array
{
$list = [];
foreach ($rows as $key => $row) {
if (null === $row || (!\is_object($row) && !\is_array($row))) {
// Only load rows which haven't been loaded before.
$key = (string)$key;
if (!$this->hasKey($key)) {
$list[$key] = null;
} else {
$path = $this->getPathFromKey($key);
$file = $this->getFile($path);
$list[$key] = $this->loadFile($file);
}
if (null !== $fetched) {
$fetched[$key] = $list[$key];
}
} else {
// Keep the row if it has been loaded.
$list[$key] = $row;
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::updateRows()
*/
public function updateRows(array $rows): array
{
$list = [];
foreach ($rows as $key => $row) {
$key = (string)$key;
if (!$this->hasKey($key)) {
$list[$key] = null;
} else {
$path = $this->getPathFromKey($key);
$file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row);
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::deleteRows()
*/
public function deleteRows(array $rows): array
{
$list = [];
foreach ($rows as $key => $row) {
$key = (string)$key;
if (!$this->hasKey($key)) {
$list[$key] = null;
} else {
$path = $this->getPathFromKey($key);
$file = $this->getFile($path);
$list[$key] = $this->deleteFile($file);
$storage = $this->getStoragePath($key);
$media = $this->getMediaPath($key);
$this->deleteFolder($storage, true);
$media && $this->deleteFolder($media, true);
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::replaceRows()
*/
public function replaceRows(array $rows): array
{
$list = [];
foreach ($rows as $key => $row) {
$key = (string)$key;
if (strpos($key, '@@')) {
$key = $this->getNewKey();
}
$path = $this->getPathFromKey($key);
$file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row);
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::renameRow()
*/
public function renameRow(string $src, string $dst): bool
{
if ($this->hasKey($dst)) {
throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken");
}
if (!$this->hasKey($src)) {
return false;
}
return $this->moveFolder($this->getMediaPath($src), $this->getMediaPath($dst));
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getStoragePath()
*/
public function getStoragePath(string $key = null): string
{
if (null === $key) {
$path = $this->dataFolder;
} else {
$path = sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
}
return $path;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getMediaPath()
*/
public function getMediaPath(string $key = null): string
{
return null !== $key ? \dirname($this->getStoragePath($key)) : $this->getStoragePath();
}
/**
* Get filesystem path from the key.
*
* @param string $key
* @return string
*/
public function getPathFromKey(string $key): string
{
return sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2));
}
/**
* @param File $file
* @return array|null
*/
protected function loadFile(File $file): ?array
{
if (!$file->exists()) {
return null;
}
try {
$content = (array)$file->content();
if (isset($content[0])) {
throw new \RuntimeException('Broken object file.');
}
} catch (\RuntimeException $e) {
$content = ['__error' => $e->getMessage()];
}
return $content;
}
/**
* @param File $file
* @param array $data
* @return array
*/
protected function saveFile(File $file, array $data): array
{
try {
$file->save($data);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($file->filename())) {
$locator->clearCache($file->filename());
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex saveFile(%s): %s', $file->filename(), $e->getMessage()));
}
return $data;
}
/**
* @param File $file
* @return array|string
*/
protected function deleteFile(File $file)
{
try {
$data = $file->content();
$file->delete();
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($file->filename())) {
$locator->clearCache($file->filename());
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex deleteFile(%s): %s', $file->filename(), $e->getMessage()));
}
return $data;
}
/**
* @param string $src
* @param string $dst
* @return bool
*/
protected function moveFolder(string $src, string $dst): bool
{
try {
Folder::move($this->resolvePath($src), $this->resolvePath($dst));
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($src) || $locator->isStream($dst)) {
$locator->clearCache();
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
}
return true;
}
/**
* @param string $path
* @param bool $include_target
* @return bool
*/
protected function deleteFolder(string $path, bool $include_target = false): bool
{
try {
$success = Folder::delete($this->resolvePath($path), $include_target);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$locator->clearCache();
}
return $success;
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage()));
}
}
/**
* Get key from the filesystem path.
*
* @param string $path
* @return string
*/
protected function getKeyFromPath(string $path): string
{
return basename($path);
}
/**
* Returns list of all stored keys in [key => timestamp] pairs.
*
* @return array
*/
protected function buildIndex(): array
{
$path = $this->getStoragePath();
if (!file_exists($path)) {
return [];
}
if ($this->prefixed) {
$list = $this->buildPrefixedIndexFromFilesystem($path);
} else {
$list = $this->buildIndexFromFilesystem($path);
}
ksort($list, SORT_NATURAL);
return $list;
}
protected function buildIndexFromFilesystem($path)
{
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
$iterator = new \FilesystemIterator($path, $flags);
$list = [];
/** @var \SplFileInfo $info */
foreach ($iterator as $filename => $info) {
if (!$info->isDir()) {
continue;
}
$key = $this->getKeyFromPath($filename);
$filename = $this->getPathFromKey($key);
$modified = is_file($filename) ? filemtime($filename) : null;
if (null === $modified) {
continue;
}
$list[$key] = [
'storage_key' => $key,
'storage_timestamp' => $modified
];
}
return $list;
}
protected function buildPrefixedIndexFromFilesystem($path)
{
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
$iterator = new \FilesystemIterator($path, $flags);
$list = [];
/** @var \SplFileInfo $info */
foreach ($iterator as $filename => $info) {
if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
continue;
}
$list[] = $this->buildIndexFromFilesystem($filename);
}
if (!$list) {
return [];
}
return \count($list) > 1 ? array_merge(...$list) : $list[0];
}
/**
* @return string
*/
protected function getNewKey(): string
{
// Make sure that the file doesn't exist.
do {
$key = $this->generateKey();
} while (file_exists($this->getPathFromKey($key)));
return $key;
}
/**
* @param array $options
*/
protected function initOptions(array $options): void
{
$extension = $this->dataFormatter->getDefaultFileExtension();
/** @var string $pattern */
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
$this->dataFolder = $options['folder'];
$this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/'));
$this->indexed = (bool)($options['indexed'] ?? false);
$this->keyField = $options['key'] ?? 'storage_key';
$pattern = preg_replace(['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/'], ['%1$s', '%2$s', '%3$s'], $pattern);
$this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension;
}
}
@@ -0,0 +1,323 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Storage;
use Grav\Common\Filesystem\Folder;
use InvalidArgumentException;
/**
* Class SimpleStorage
* @package Grav\Framework\Flex\Storage
*/
class SimpleStorage extends AbstractFilesystemStorage
{
/** @var string */
protected $dataFolder;
/** @var string */
protected $dataPattern;
/** @var array */
protected $data;
/**
* {@inheritdoc}
* @see FlexStorageInterface::__construct()
*/
public function __construct(array $options)
{
if (!isset($options['folder'])) {
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
}
$formatter = $options['formatter'] ?? $this->detectDataFormatter($options['folder']);
$this->initDataFormatter($formatter);
$extension = $this->dataFormatter->getDefaultFileExtension();
$pattern = basename($options['folder']);
$this->dataPattern = basename($pattern, $extension) . $extension;
$this->dataFolder = \dirname($options['folder']);
// Make sure that the data folder exists.
if (!file_exists($this->dataFolder)) {
try {
Folder::create($this->dataFolder);
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage()));
}
}
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getExistingKeys()
*/
public function getExistingKeys(): array
{
return $this->buildIndex();
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::hasKey()
*/
public function hasKey(string $key): bool
{
if (null === $this->data) {
$this->buildIndex();
}
return $key && strpos($key, '@@') === false && isset($this->data[$key]);
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::createRows()
*/
public function createRows(array $rows): array
{
if (null === $this->data) {
$this->buildIndex();
}
$list = [];
foreach ($rows as $key => $row) {
$key = $this->getNewKey();
$this->data[$key] = $list[$key] = $row;
}
if ($list) {
$this->save();
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::readRows()
*/
public function readRows(array $rows, array &$fetched = null): array
{
if (null === $this->data) {
$this->buildIndex();
}
$list = [];
foreach ($rows as $key => $row) {
if (null === $row || (!\is_object($row) && !\is_array($row))) {
// Only load rows which haven't been loaded before.
$key = (string)$key;
$list[$key] = $this->hasKey($key) ? $this->data[$key] : null;
if (null !== $fetched) {
$fetched[$key] = $list[$key];
}
} else {
// Keep the row if it has been loaded.
$list[$key] = $row;
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::updateRows()
*/
public function updateRows(array $rows): array
{
if (null === $this->data) {
$this->buildIndex();
}
$list = [];
foreach ($rows as $key => $row) {
$key = (string)$key;
if ($this->hasKey($key)) {
$this->data[$key] = $list[$key] = $row;
}
}
if ($list) {
$this->save();
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::deleteRows()
*/
public function deleteRows(array $rows): array
{
if (null === $this->data) {
$this->buildIndex();
}
$list = [];
foreach ($rows as $key => $row) {
$key = (string)$key;
if ($this->hasKey($key)) {
unset($this->data[$key]);
$list[$key] = $row;
}
}
if ($list) {
$this->save();
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::replaceRows()
*/
public function replaceRows(array $rows): array
{
if (null === $this->data) {
$this->buildIndex();
}
$list = [];
foreach ($rows as $key => $row) {
if (strpos($key, '@@')) {
$key = $this->getNewKey();
}
$this->data[$key] = $list[$key] = $row;
}
if ($list) {
$this->save();
}
return $list;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::renameRow()
*/
public function renameRow(string $src, string $dst): bool
{
if (null === $this->data) {
$this->buildIndex();
}
if ($this->hasKey($dst)) {
throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken");
}
if (!$this->hasKey($src)) {
return false;
}
// Change single key in the array without changing the order or value.
$keys = array_keys($this->data);
$keys[array_search($src, $keys, true)] = $dst;
$data = array_combine($keys, $this->data);
if (false === $data) {
throw new \LogicException('Bad data');
}
$this->data = $data;
return true;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getStoragePath()
*/
public function getStoragePath(string $key = null): string
{
return $this->dataFolder . '/' . $this->dataPattern;
}
/**
* {@inheritdoc}
* @see FlexStorageInterface::getMediaPath()
*/
public function getMediaPath(string $key = null): string
{
return sprintf('%s/%s/%s', $this->dataFolder, basename($this->dataPattern, $this->dataFormatter->getDefaultFileExtension()), $key);
}
protected function save() : void
{
if (null === $this->data) {
$this->buildIndex();
}
try {
$file = $this->getFile($this->getStoragePath());
$file->save($this->data);
$file->free();
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex save(): %s', $e->getMessage()));
}
}
/**
* Get key from the filesystem path.
*
* @param string $path
* @return string
*/
protected function getKeyFromPath(string $path): string
{
return basename($path);
}
/**
* Returns list of all stored keys in [key => timestamp] pairs.
*
* @return array
*/
protected function buildIndex(): array
{
$file = $this->getFile($this->getStoragePath());
$modified = $file->modified();
$this->data = (array) $file->content();
$list = [];
foreach ($this->data as $key => $info) {
$list[$key] = [
'storage_key' => $key,
'storage_timestamp' => $modified
];
}
return $list;
}
/**
* @return string
*/
protected function getNewKey(): string
{
if (null === $this->data) {
$this->buildIndex();
}
// Make sure that the key doesn't exist.
do {
$key = $this->generateKey();
} while (isset($this->data[$key]));
return $key;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex\Traits;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
/**
* Implements basic ACL
*/
trait FlexAuthorizeTrait
{
private $_authorize = '%s.flex-object.%s';
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool
{
if (null === $user) {
/** @var UserInterface $user */
$user = Grav::instance()['user'] ?? null;
}
return $user && ($this->isAuthorizedAction($user, $action, $scope) || $this->isAuthorizedSuperAdmin($user));
}
protected function isAuthorizedSuperAdmin(UserInterface $user): bool
{
return $user->authorize('admin.super');
}
protected function isAuthorizedAction(UserInterface $user, string $action, string $scope = null) : bool
{
$scope = $scope ?? isset(Grav::instance()['admin']) ? 'admin' : 'site';
if ($action === 'save' && $this instanceof FlexObjectInterface) {
$action = $this->exists() ? 'update' : 'create';
}
$directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory();
$config = $directory->getConfig();
$allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true;
return $allowed && $user->authorize(sprintf($this->_authorize, $scope, $action));
}
protected function setAuthorizeRule(string $authorize) : void
{
$this->_authorize = $authorize;
}
}
@@ -0,0 +1,346 @@
<?php
namespace Grav\Framework\Flex\Traits;
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\Utils;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Form\FormFlashFile;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
/**
* Implements Grav Page content and header manipulation methods.
*/
trait FlexMediaTrait
{
use MediaTrait {
MediaTrait::getMedia as protected getExistingMedia;
}
protected $_uploads;
public function __debugInfo()
{
return parent::__debugInfo() + [
'uploads:private' => $this->getUpdatedMedia()
];
}
/**
* @return string
*/
public function getStorageFolder()
{
return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : '';
}
/**
* @return string
*/
public function getMediaFolder()
{
return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : '';
}
/**
* @return MediaCollectionInterface
*/
public function getMedia()
{
if ($this->media === null) {
/** @var AbstractMedia $media */
$media = $this->getExistingMedia();
// Include uploaded media to the object media.
/** @var FormFlashFile $upload */
foreach ($this->getUpdatedMedia() as $filename => $upload) {
// Just make sure we do not include removed or moved media.
if ($upload && $upload->getError() === \UPLOAD_ERR_OK && !$upload->isMoved()) {
$media->add($filename, MediumFactory::fromUploadedFile($upload));
}
}
$media->setTimestamps();
}
return $this->media;
}
public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile)
{
$grav = Grav::instance();
$language = $grav['language'];
switch ($uploadedFile->getError()) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
if ($uploadedFile instanceof FormFlashFile) {
break;
}
throw new RuntimeException($language->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400);
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
case UPLOAD_ERR_NO_TMP_DIR:
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400);
default:
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
}
$filename = $uploadedFile->getClientFilename();
if (!Utils::checkFilename($filename)) {
throw new RuntimeException(sprintf($language->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filename, 'Bad filename'), 400);
}
$grav_limit = Utils::getUploadLimit();
if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
}
$this->checkMediaFilename($filename);
}
public function checkMediaFilename(string $filename)
{
// Check the file extension.
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
// If not a supported type, return
if (!$extension || !$config->get("media.types.{$extension}")) {
$language = $grav['language'];
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
}
}
public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null): void
{
$this->checkUploadedMediaFile($uploadedFile);
if ($filename) {
$this->checkMediaFilename(basename($filename));
} else {
$filename = $uploadedFile->getClientFilename();
}
$media = $this->getMedia();
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$path = $media->getPath();
if (!$path) {
$language = $grav['language'];
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
}
if ($locator->isStream($path)) {
$path = $locator->findResource($path, true, true);
$locator->clearCache($path);
}
try {
// Upload it
$filepath = sprintf('%s/%s', $path, $filename);
Folder::create(\dirname($filepath));
if ($uploadedFile instanceof FormFlashFile) {
$metadata = $uploadedFile->getMetaData();
if ($metadata) {
$file = YamlFile::instance($filepath . '.meta.yaml');
$file->save(['upload' => $metadata]);
}
if ($uploadedFile->getError() === \UPLOAD_ERR_OK) {
$uploadedFile->moveTo($filepath);
} elseif (!file_exists($filepath) && $pos = strpos($filename, '/')) {
$origpath = sprintf('%s/%s', $path, substr($filename, $pos));
if (file_exists($origpath)) {
copy($origpath, $filepath);
}
}
} else {
$uploadedFile->moveTo($filepath);
}
} catch (\Exception $e) {
$language = $grav['language'];
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
}
$this->clearMediaCache();
}
public function deleteMediaFile(string $filename): void
{
$grav = Grav::instance();
$language = $grav['language'];
$basename = basename($filename);
$dirname = dirname($filename);
$dirname = $dirname === '.' ? '' : '/' . $dirname;
if (!Utils::checkFilename($basename)) {
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': Bad filename: ' . $filename, 400);
}
$media = $this->getMedia();
$path = $media->getPath();
if (!$path) {
return;
}
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$targetPath = $path . '/' . $dirname;
$targetFile = $path . '/' . $filename;
if ($locator->isStream($targetFile)) {
$targetPath = $locator->findResource($targetPath, true, true);
$targetFile = $locator->findResource($targetFile, true, true);
$locator->clearCache($targetPath);
$locator->clearCache($targetFile);
}
$fileParts = pathinfo($basename);
if (!file_exists($targetPath)) {
return;
}
if (file_exists($targetFile)) {
$result = unlink($targetFile);
if (!$result) {
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
}
}
// Remove Extra Files
foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) {
$preg_name = preg_quote($fileParts['filename'], '`');
$preg_ext =preg_quote($fileParts['extension'], '`');
$preg_filename = preg_quote($basename, '`');
if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) {
$testPath = $targetPath . '/' . $file;
if ($locator->isStream($testPath)) {
$testPath = $locator->findResource($testPath, true, true);
$locator->clearCache($testPath);
}
if (is_file($testPath)) {
$result = unlink($testPath);
if (!$result) {
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
}
}
}
}
$this->clearMediaCache();
}
/**
* @param array $files
*/
protected function setUpdatedMedia(array $files): void
{
$list = [];
foreach ($files as $field => $group) {
if ($field === '' || \strpos($field, '/', true)) {
continue;
}
foreach ($group as $filename => $file) {
$list[$filename] = $file;
}
}
$this->_uploads = $list;
}
/**
* @return array
*/
protected function getUpdatedMedia(): array
{
return $this->_uploads ?? [];
}
protected function saveUpdatedMedia(): void
{
/**
* @var string $filename
* @var UploadedFileInterface $file
*/
foreach ($this->getUpdatedMedia() as $filename => $file) {
if ($file) {
$this->uploadMediaFile($file, $filename);
} else {
$this->deleteMediaFile($filename);
}
}
$this->setUpdatedMedia([]);
}
/**
* @param string $uri
* @return Medium|null
*/
protected function createMedium($uri)
{
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri;
return $file && file_exists($file) ? MediumFactory::fromFile($file) : null;
}
/**
* @return Cache
*/
protected function getMediaCache()
{
return $this->getCache('object');
}
protected function offsetLoad_media()
{
return $this->getMedia();
}
protected function offsetSerialize_media()
{
return null;
}
abstract public function getFlexDirectory(): FlexDirectory;
abstract public function getStorageKey();
}
@@ -0,0 +1,519 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class FormFlash implements FormFlashInterface
{
/** @var bool */
protected $exists;
/** @var string */
protected $sessionId;
/** @var string */
protected $uniqueId;
/** @var string */
protected $formName;
/** @var string */
protected $url;
/** @var array */
protected $user;
/** @var int */
protected $createdTimestamp;
/** @var int */
protected $updatedTimestamp;
/** @var array */
protected $data;
/** @var array */
protected $files;
/** @var array */
protected $uploadedFiles;
/** @var string[] */
protected $uploadObjects;
/** @var string */
protected $folder;
/**
* @inheritDoc
*/
public function __construct($config)
{
// Backwards compatibility with Grav 1.6 plugins.
if (!is_array($config)) {
user_error(__CLASS__ . '::' . __FUNCTION__ . '($sessionId, $uniqueId, $formName) is deprecated since Grav 1.6.11, use $config parameter instead', E_USER_DEPRECATED);
$args = func_get_args();
$config = [
'session_id' => $args[0],
'unique_id' => $args[1] ?? null,
'form_name' => $args[2] ?? null,
];
}
$this->sessionId = $config['session_id'] ?? 'no-session';
$this->uniqueId = $config['unique_id'] ?? '';
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;
$file = $this->getTmpIndex();
$this->exists = $file->exists();
if ($this->exists) {
try {
$data = (array)$file->content();
} catch (\Exception $e) {
$data = [];
}
$this->formName = $content['form'] ?? $config['form_name'] ?? '';
$this->url = $data['url'] ?? '';
$this->user = $data['user'] ?? null;
$this->updatedTimestamp = $data['timestamps']['updated'] ?? time();
$this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp;
$this->data = $data['data'] ?? null;
$this->files = $data['files'] ?? [];
} else {
$this->formName = $config['form_name'] ?? '';
$this->url = '';
$this->createdTimestamp = $this->updatedTimestamp = time();
$this->files = [];
}
}
/**
* @inheritDoc
*/
public function getSessionId(): string
{
return $this->sessionId;
}
/**
* @inheritDoc
*/
public function getUniqueId(): string
{
return $this->uniqueId;
}
/**
* @deprecated 1.6.11 Use '->getUniqueId()' method instead.
*/
public function getUniqieId(): string
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED);
return $this->getUniqueId();
}
/**
* @inheritDoc
*/
public function getFormName(): string
{
return $this->formName;
}
/**
* @inheritDoc
*/
public function getUrl(): string
{
return $this->url;
}
/**
* @inheritDoc
*/
public function getUsername(): string
{
return $this->user['username'] ?? '';
}
/**
* @inheritDoc
*/
public function getUserEmail(): string
{
return $this->user['email'] ?? '';
}
/**
* @inheritDoc
*/
public function getCreatedTimestamp(): int
{
return $this->createdTimestamp;
}
/**
* @inheritDoc
*/
public function getUpdatedTimestamp(): int
{
return $this->updatedTimestamp;
}
/**
* @inheritDoc
*/
public function getData(): ?array
{
return $this->data;
}
/**
* @inheritDoc
*/
public function setData(?array $data): void
{
$this->data = $data;
}
/**
* @inheritDoc
*/
public function exists(): bool
{
return $this->exists;
}
/**
* @inheritDoc
*/
public function save(): self
{
if (!($this->folder && $this->uniqueId)) {
return $this;
}
if ($this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
}
return $this;
}
/**
* @inheritDoc
*/
public function delete(): self
{
if ($this->folder && $this->uniqueId) {
$this->removeTmpDir();
$this->files = [];
$this->exists = false;
}
return $this;
}
/**
* @inheritDoc
*/
public function getFilesByField(string $field): array
{
if (!isset($this->uploadObjects[$field])) {
$objects = [];
foreach ($this->files[$field] ?? [] as $name => $upload) {
$objects[$name] = $upload ? new FormFlashFile($field, $upload, $this) : null;
}
$this->uploadedFiles[$field] = $objects;
}
return $this->uploadedFiles[$field];
}
/**
* @inheritDoc
*/
public function getFilesByFields($includeOriginal = false): array
{
$list = [];
foreach ($this->files as $field => $values) {
if (!$includeOriginal && strpos($field, '/')) {
continue;
}
$list[$field] = $this->getFilesByField($field);
}
return $list;
}
/**
* @inheritDoc
*/
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string
{
$tmp_dir = $this->getTmpDir();
$tmp_name = Utils::generateRandomString(12);
$name = $upload->getClientFilename();
// Prepare upload data for later save
$data = [
'name' => $name,
'type' => $upload->getClientMediaType(),
'size' => $upload->getSize(),
'tmp_name' => $tmp_name
];
Folder::create($tmp_dir);
$upload->moveTo("{$tmp_dir}/{$tmp_name}");
$this->addFileInternal($field, $name, $data, $crop);
return $name;
}
/**
* @inheritDoc
*/
public function addFile(string $filename, string $field, array $crop = null): bool
{
if (!file_exists($filename)) {
throw new \RuntimeException("File not found: {$filename}");
}
// Prepare upload data for later save
$data = [
'name' => basename($filename),
'type' => Utils::getMimeByLocalFile($filename),
'size' => filesize($filename),
];
$this->addFileInternal($field, $data['name'], $data, $crop);
return true;
}
/**
* @inheritDoc
*/
public function removeFile(string $name, string $field = null): bool
{
if (!$name) {
return false;
}
$field = $field ?: 'undefined';
$upload = $this->files[$field][$name] ?? null;
if (null !== $upload) {
$this->removeTmpFile($upload['tmp_name'] ?? '');
}
$upload = $this->files[$field . '/original'][$name] ?? null;
if (null !== $upload) {
$this->removeTmpFile($upload['tmp_name'] ?? '');
}
// Mark file as deleted.
$this->files[$field][$name] = null;
$this->files[$field . '/original'][$name] = null;
unset(
$this->uploadedFiles[$field][$name],
$this->uploadedFiles[$field . '/original'][$name]
);
return true;
}
/**
* @inheritDoc
*/
public function clearFiles()
{
foreach ($this->files as $field => $files) {
foreach ($files as $name => $upload) {
$this->removeTmpFile($upload['tmp_name'] ?? '');
}
}
$this->files = [];
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
return [
'form' => $this->formName,
'unique_id' => $this->uniqueId,
'url' => $this->url,
'user' => $this->user,
'timestamps' => [
'created' => $this->createdTimestamp,
'updated' => time(),
],
'data' => $this->data,
'files' => $this->files
];
}
/**
* @param string $url
* @return $this
*/
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
/**
* @param UserInterface|null $user
* @return $this
*/
public function setUser(UserInterface $user = null)
{
if ($user && $user->username) {
$this->user = [
'username' => $user->username,
'email' => $user->email ?? ''
];
} else {
$this->user = null;
}
return $this;
}
/**
* @param string|null $username
* @return $this
*/
public function setUserName(string $username = null): self
{
$this->user['username'] = $username;
return $this;
}
/**
* @param string|null $email
* @return $this
*/
public function setUserEmail(string $email = null): self
{
$this->user['email'] = $email;
return $this;
}
/**
* @return string
*/
public function getTmpDir(): string
{
return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : '';
}
/**
* @return YamlFile
*/
protected function getTmpIndex(): YamlFile
{
// Do not use CompiledYamlFile as the file can change multiple times per second.
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
}
/**
* @param string $name
*/
protected function removeTmpFile(string $name): void
{
$tmpDir = $this->getTmpDir();
$filename = $tmpDir ? $tmpDir . '/' . $name : '';
if ($name && $filename && is_file($filename)) {
unlink($filename);
}
}
protected function removeTmpDir(): void
{
$tmpDir = $this->getTmpDir();
if ($tmpDir && file_exists($tmpDir)) {
Folder::delete($tmpDir);
}
}
/**
* @param string $field
* @param string $name
* @param array $data
* @param array|null $crop
*/
protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void
{
if (!($this->folder && $this->uniqueId)) {
throw new \RuntimeException('Cannot upload files: form flash folder not defined');
}
$field = $field ?: 'undefined';
if (!isset($this->files[$field])) {
$this->files[$field] = [];
}
$oldUpload = $this->files[$field][$name] ?? null;
if ($crop) {
// Deal with crop upload
if ($oldUpload) {
$originalUpload = $this->files[$field . '/original'][$name] ?? null;
if ($originalUpload) {
// If there is original file already present, remove the modified file
$this->files[$field . '/original'][$name]['crop'] = $crop;
$this->removeTmpFile($oldUpload['tmp_name'] ?? '');
} else {
// Otherwise make the previous file as original
$oldUpload['crop'] = $crop;
$this->files[$field . '/original'][$name] = $oldUpload;
}
} else {
$this->files[$field . '/original'][$name] = [
'name' => $name,
'type' => $data['type'],
'crop' => $crop
];
}
} else {
// Deal with replacing upload
$originalUpload = $this->files[$field . '/original'][$name] ?? null;
$this->files[$field . '/original'][$name] = null;
$this->removeTmpFile($oldUpload['tmp_name'] ?? '');
$this->removeTmpFile($originalUpload['tmp_name'] ?? '');
}
// Prepare data to be saved later
$this->files[$field][$name] = $data;
}
}
@@ -0,0 +1,159 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form;
use Grav\Framework\Psr7\Stream;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
class FormFlashFile implements UploadedFileInterface, \JsonSerializable
{
private $field;
private $moved = false;
private $upload;
private $flash;
public function __construct(string $field, array $upload, FormFlash $flash)
{
$this->field = $field;
$this->upload = $upload;
$this->flash = $flash;
$tmpFile = $this->getTmpFile();
if (!$tmpFile && $this->isOk()) {
$this->upload['error'] = \UPLOAD_ERR_NO_FILE;
}
if (!isset($this->upload['size'])) {
$this->upload['size'] = $tmpFile && $this->isOk() ? filesize($tmpFile) : 0;
}
}
/**
* @return StreamInterface
*/
public function getStream()
{
$this->validateActive();
$resource = \fopen($this->getTmpFile(), 'rb');
return Stream::create($resource);
}
public function moveTo($targetPath)
{
$this->validateActive();
if (!\is_string($targetPath) || empty($targetPath)) {
throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
}
$this->moved = \copy($this->getTmpFile(), $targetPath);
if (false === $this->moved) {
throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
}
$this->flash->removeFile($this->getClientFilename(), $this->field);
}
public function getSize()
{
return $this->upload['size'];
}
public function getError()
{
return $this->upload['error'] ?? \UPLOAD_ERR_OK;
}
public function getClientFilename()
{
return $this->upload['name'] ?? 'unknown';
}
public function getClientMediaType()
{
return $this->upload['type'] ?? 'application/octet-stream';
}
public function isMoved() : bool
{
return $this->moved;
}
public function getMetaData() : array
{
if (isset($this->upload['crop'])) {
return ['crop' => $this->upload['crop']];
}
return [];
}
public function getDestination()
{
return $this->upload['path'] ?? '';
}
public function jsonSerialize()
{
return $this->upload;
}
public function getTmpFile() : ?string
{
$tmpName = $this->upload['tmp_name'] ?? null;
if (!$tmpName) {
return null;
}
$tmpFile = $this->flash->getTmpDir() . '/' . $tmpName;
return file_exists($tmpFile) ? $tmpFile : null;
}
public function __debugInfo()
{
return [
'field:private' => $this->field,
'moved:private' => $this->moved,
'upload:private' => $this->upload,
];
}
/**
* @throws \RuntimeException if is moved or not ok
*/
private function validateActive(): void
{
if (!$this->isOk()) {
throw new \RuntimeException('Cannot retrieve stream due to upload error');
}
if ($this->moved) {
throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
}
if (!$this->getTmpFile()) {
throw new \RuntimeException('Cannot retrieve stream as the file is missing');
}
}
/**
* @return bool return true if there is no upload error
*/
private function isOk(): bool
{
return \UPLOAD_ERR_OK === $this->getError();
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form\Interfaces;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Page;
interface FormFactoryInterface
{
/**
* @param Page $page
* @param string $name
* @param array $form
* @return FormInterface|null
* @deprecated 1.6 Use FormFactory::createFormByPage() instead.
*/
public function createPageForm(Page $page, string $name, array $form): ?FormInterface;
/**
* Create form using the header of the page.
*
* @param PageInterface $page
* @param string $name
* @param array $form
* @return FormInterface|null
*
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface;
*/
}
@@ -0,0 +1,165 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form\Interfaces;
use Psr\Http\Message\UploadedFileInterface;
interface FormFlashInterface extends \JsonSerializable
{
/**
* @param array $config Available configuration keys: session_id, unique_id, form_name
*/
public function __construct($config);
/**
* Get session Id associated to this form instance.
*
* @return string
*/
public function getSessionId(): string;
/**
* Get unique identifier associated to this form instance.
*
* @return string
*/
public function getUniqueId(): string;
/**
* Get form name associated to this form instance.
*
* @return string
*/
public function getFormName(): string;
/**
* Get URL associated to this form instance.
*
* @return string
*/
public function getUrl(): string;
/**
* Get username from the user who was associated to this form instance.
*
* @return string
*/
public function getUsername(): string;
/**
* Get email from the user who was associated to this form instance.
*
* @return string
*/
public function getUserEmail(): string;
/**
* Get creation timestamp for this form flash.
*
* @return int
*/
public function getCreatedTimestamp(): int;
/**
* Get last updated timestamp for this form flash.
*
* @return int
*/
public function getUpdatedTimestamp(): int;
/**
* Get raw form data.
*
* @return array|null
*/
public function getData(): ?array;
/**
* Set raw form data.
*
* @param array|null $data
*/
public function setData(?array $data): void;
/**
* Check if this form flash exists.
*
* @return bool
*/
public function exists(): bool;
/**
* Save this form flash.
*
* @return $this
*/
public function save();
/**
* Delete this form flash.
*/
public function delete();
/**
* Get all files associated to a form field.
*
* @param string $field
* @return array
*/
public function getFilesByField(string $field): array;
/**
* Get all files grouped by the associated form fields.
*
* @param bool $includeOriginal
* @return array
*/
public function getFilesByFields($includeOriginal = false): array;
/**
* Add uploaded file to the form flash.
*
* @param UploadedFileInterface $upload
* @param string|null $field
* @param array|null $crop
* @return string Return name of the file
*/
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string;
/**
* Add existing file to the form flash.
*
* @param string $filename
* @param string $field
* @param array $crop
* @return bool
*/
public function addFile(string $filename, string $field, array $crop = null): bool;
/**
* Remove any file from form flash.
*
* @param string $name
* @param string $field
* @return bool
*/
public function removeFile(string $name, string $field = null): bool;
/**
* Clear form flash from all uploaded files.
*/
public function clearFiles();
/**
* @return array
*/
public function jsonSerialize(): array;
}
@@ -0,0 +1,178 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form\Interfaces;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Framework\Interfaces\RenderInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* Interface FormInterface
* @package Grav\Framework\Form
*/
interface FormInterface extends RenderInterface, \Serializable
{
/**
* Get HTML id="..." attribute.
*
* @return string
*/
public function getId(): string;
/**
* Sets HTML id="" attribute.
*
* @param string $id
*/
public function setId(string $id): void;
/**
* Get unique id for the current form instance. By default regenerated on every page reload.
*
* This id is used to load the saved form state, if available.
*
* @return string
*/
public function getUniqueId(): string;
/**
* Sets unique form id.
*
* @param string $uniqueId
*/
public function setUniqueId(string $uniqueId): void;
/**
* @return string
*/
public function getName(): string;
/**
* Get form name.
*
* @return string
*/
public function getFormName(): string;
/**
* Get nonce name.
*
* @return string
*/
public function getNonceName(): string;
/**
* Get nonce action.
*
* @return string
*/
public function getNonceAction(): string;
/**
* Get the nonce value for a form
*
* @return string
*/
public function getNonce(): string;
/**
* Get task for the form if set in blueprints.
*
* @return string
*/
public function getTask(): string;
/**
* Get form action (URL). If action is empty, it points to the current page.
*
* @return string
*/
public function getAction(): string;
/**
* Get current data passed to the form.
*
* @return Data|object
*/
public function getData();
/**
* Get files which were passed to the form.
*
* @return array|UploadedFileInterface[]
*/
public function getFiles(): array;
/**
* Get a value from the form.
*
* Note: Used in form fields.
*
* @param string $name
* @return mixed
*/
public function getValue(string $name);
/**
* @param ServerRequestInterface $request
* @return $this
*/
public function handleRequest(ServerRequestInterface $request): FormInterface;
/**
* @param array $data
* @param UploadedFileInterface[] $files
* @return $this
*/
public function submit(array $data, array $files = null): FormInterface;
/**
* @return bool
*/
public function isValid(): bool;
/**
* @return string
*/
public function getError(): ?string;
/**
* @return array
*/
public function getErrors(): array;
/**
* @return bool
*/
public function isSubmitted(): bool;
/**
* Reset form.
*/
public function reset(): void;
/**
* Get form fields as an array.
*
* Note: Used in form fields.
*
* @return array
*/
public function getFields(): array;
/**
* Get blueprint used in the form.
*
* @return Blueprint
*/
public function getBlueprint(): Blueprint;
}
@@ -0,0 +1,692 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form\Traits;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Data\ValidationException;
use Grav\Common\Form\FormFlash;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Session\SessionInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\TemplateWrapper;
/**
* Trait FormTrait
* @package Grav\Framework\Form
*/
trait FormTrait
{
/** @var string */
public $status = 'success';
/** @var string */
public $message;
/** @var string[] */
public $messages = [];
/** @var string */
private $name;
/** @var string */
private $id;
/** @var string */
private $uniqueid;
/** @var bool */
private $submitted;
/** @var Data|object|null */
private $data;
/** @var array|UploadedFileInterface[] */
private $files;
/** @var FormFlash|null */
private $flash;
/** @var Blueprint */
private $blueprint;
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getUniqueId(): string
{
return $this->uniqueid;
}
public function setUniqueId(string $uniqueId): void
{
$this->uniqueid = $uniqueId;
}
public function getName(): string
{
return $this->name;
}
public function getFormName(): string
{
return $this->name;
}
public function getNonceName(): string
{
return 'form-nonce';
}
public function getNonceAction(): string
{
return 'form';
}
public function getNonce(): string
{
return Utils::getNonce($this->getNonceAction());
}
public function getAction(): string
{
return '';
}
public function getTask(): string
{
return $this->getBlueprint()->get('form/task') ?? '';
}
public function getData(string $name = null)
{
return null !== $name ? $this->data[$name] : $this->data;
}
/**
* @return array|UploadedFileInterface[]
*/
public function getFiles(): array
{
return $this->files ?? [];
}
public function getValue(string $name)
{
return $this->data[$name] ?? null;
}
public function getDefaultValue(string $name)
{
$path = explode('.', $name) ?: [];
$offset = array_shift($path) ?? '';
$current = $this->getDefaultValues();
if (!isset($current[$offset])) {
return null;
}
$current = $current[$offset];
while ($path) {
$offset = array_shift($path);
if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
$current = $current[$offset];
} elseif (\is_object($current) && isset($current->{$offset})) {
$current = $current->{$offset};
} else {
return null;
}
};
return $current;
}
/**
* @return array
*/
public function getDefaultValues(): array
{
return $this->getBlueprint()->getDefaults();
}
/**
* @param ServerRequestInterface $request
* @return FormInterface|$this
*/
public function handleRequest(ServerRequestInterface $request): FormInterface
{
// Set current form to be active.
$grav = Grav::instance();
$forms = $grav['forms'] ?? null;
if ($forms) {
$forms->setActiveForm($this);
/** @var Twig $twig */
$twig = $grav['twig'];
$twig->twig_vars['form'] = $this;
}
try {
[$data, $files] = $this->parseRequest($request);
$this->submit($data, $files);
} catch (\Exception $e) {
$this->setError($e->getMessage());
}
return $this;
}
/**
* @param ServerRequestInterface $request
* @return FormInterface|$this
*/
public function setRequest(ServerRequestInterface $request): FormInterface
{
[$data, $files] = $this->parseRequest($request);
$this->data = new Data($data, $this->getBlueprint());
$this->files = $files;
return $this;
}
public function isValid(): bool
{
return $this->status === 'success';
}
public function getError(): ?string
{
return !$this->isValid() ? $this->message : null;
}
public function getErrors(): array
{
return !$this->isValid() ? $this->messages : [];
}
public function isSubmitted(): bool
{
return $this->submitted;
}
public function validate(): bool
{
if (!$this->isValid()) {
return false;
}
try {
$this->validateData($this->data);
$this->validateUploads($this->getFiles());
} catch (ValidationException $e) {
$this->setErrors($e->getMessages());
} catch (\Exception $e) {
$this->setError($e->getMessage());
}
$this->filterData($this->data);
return $this->isValid();
}
/**
* @param array $data
* @param UploadedFileInterface[] $files
* @return FormInterface|$this
*/
public function submit(array $data, array $files = null): FormInterface
{
try {
if ($this->isSubmitted()) {
throw new \RuntimeException('Form has already been submitted');
}
$this->data = new Data($data, $this->getBlueprint());
$this->files = $files ?? [];
if (!$this->validate()) {
return $this;
}
$this->doSubmit($this->data->toArray(), $this->files);
$this->submitted = true;
} catch (\Exception $e) {
$this->setError($e->getMessage());
}
return $this;
}
public function reset(): void
{
// Make sure that the flash object gets deleted.
$this->getFlash()->delete();
$this->data = null;
$this->files = [];
$this->status = 'success';
$this->message = null;
$this->messages = [];
$this->submitted = false;
$this->flash = null;
}
public function getFields(): array
{
return $this->getBlueprint()->fields();
}
public function getButtons(): array
{
return $this->getBlueprint()->get('form/buttons') ?? [];
}
public function getTasks(): array
{
return $this->getBlueprint()->get('form/tasks') ?? [];
}
abstract public function getBlueprint(): Blueprint;
/**
* Implements \Serializable::serialize().
*
* @return string
*/
public function serialize(): string
{
return serialize($this->doSerialize());
}
/**
* Implements \Serializable::unserialize().
*
* @param string $serialized
*/
public function unserialize($serialized): void
{
$data = unserialize($serialized, ['allowed_classes' => false]);
$this->doUnserialize($data);
}
/**
* Get form flash object.
*
* @return FormFlash
*/
public function getFlash(): FormFlash
{
if (null === $this->flash) {
$grav = Grav::instance();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder()
];
$this->flash = new FormFlash($config);
$this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
}
return $this->flash;
}
/**
* Get all available form flash objects for this form.
*
* @return FormFlash[]
*/
public function getAllFlashes(): array
{
$folder = $this->getFlashFolder();
if (!$folder || !is_dir($folder)) {
return [];
}
$name = $this->getName();
$list = [];
/** @var \SplFileInfo $file */
foreach (new \FilesystemIterator($folder) as $file) {
$uniqueId = $file->getFilename();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $uniqueId,
'form_name' => $name,
'folder' => $this->getFlashFolder()
];
$flash = new FormFlash($config);
if ($flash->exists() && $flash->getFormName() === $name) {
$list[] = $flash;
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FormInterface::render()
*/
public function render(string $layout = null, array $context = [])
{
if (null === $layout) {
$layout = 'default';
}
$grav = Grav::instance();
$block = HtmlBlock::create();
$block->disableCache();
$output = $this->getTemplate($layout)->render(
['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context
);
$block->setContent($output);
return $block;
}
protected function getSessionId(): string
{
/** @var Grav $grav */
$grav = Grav::instance();
/** @var SessionInterface $session */
$session = $grav['session'] ?? null;
return $session ? ($session->getId() ?? '') : '';
}
protected function unsetFlash(): void
{
$this->flash = null;
}
protected function getFlashFolder(): ?string
{
$grav = Grav::instance();
/** @var UserInterface $user */
$user = $grav['user'] ?? null;
$userExists = $user && $user->exists();
$username = $userExists ? $user->username : null;
$mediaFolder = $userExists ? $user->getMediaFolder() : null;
$session = $grav['session'] ?? null;
$sessionId = $session ? $session->getId() : null;
// Fill template token keys/value pairs.
$dataMap = [
'[FORM_NAME]' => $this->getName(),
'[SESSIONID]' => $sessionId ?? '!!',
'[USERNAME]' => $username ?? '!!',
'[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
'[ACCOUNT]' => $mediaFolder ?? '!!'
];
$flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]');
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder);
// Make sure we only return valid paths.
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/**
* Set a single error.
*
* @param string $error
*/
protected function setError(string $error): void
{
$this->status = 'error';
$this->message = $error;
}
/**
* Set all errors.
*
* @param array $errors
*/
protected function setErrors(array $errors): void
{
$this->status = 'error';
$this->messages = $errors;
}
/**
* @param string $layout
* @return TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
protected function getTemplate($layout)
{
$grav = Grav::instance();
/** @var Twig $twig */
$twig = $grav['twig'];
return $twig->twig()->resolveTemplate(
[
"forms/{$layout}/form.html.twig",
'forms/default/form.html.twig'
]
);
}
/**
* Parse PSR-7 ServerRequest into data and files.
*
* @param ServerRequestInterface $request
* @return array
*/
protected function parseRequest(ServerRequestInterface $request): array
{
$method = $request->getMethod();
if (!\in_array($method, ['PUT', 'POST', 'PATCH'])) {
throw new \RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
}
$body = $request->getParsedBody();
$data = isset($body['data']) ? $this->decodeData($body['data']) : null;
$flash = $this->getFlash();
/*
if (null !== $data) {
$flash->setData($data);
$flash->save();
}
*/
$blueprint = $this->getBlueprint();
$includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null);
$files = $flash->getFilesByFields($includeOriginal);
$data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []);
return [
$data,
$files ?? []
];
}
/**
* Form submit logic goes here.
*
* @param array $data
* @param array $files
* @return mixed
*/
abstract protected function doSubmit(array $data, array $files);
/**
* Validate data and throw validation exceptions if validation fails.
*
* @param \ArrayAccess $data
* @throws ValidationException
* @throws \Exception
*/
protected function validateData(\ArrayAccess $data): void
{
if ($data instanceof Data) {
$data->validate();
}
}
/**
* Filter validated data.
*
* @param \ArrayAccess $data
*/
protected function filterData(\ArrayAccess $data): void
{
if ($data instanceof Data) {
$data->filter();
}
}
/**
* Validate all uploaded files.
*
* @param array $files
*/
protected function validateUploads(array $files): void
{
foreach ($files as $file) {
if (null === $file) {
continue;
}
if ($file instanceof UploadedFileInterface) {
$this->validateUpload($file);
} else {
$this->validateUploads($file);
}
}
}
/**
* Validate uploaded file.
*
* @param UploadedFileInterface $file
*/
protected function validateUpload(UploadedFileInterface $file): void
{
// Handle bad filenames.
$filename = $file->getClientFilename();
if (!Utils::checkFilename($filename)) {
$grav = Grav::instance();
throw new \RuntimeException(
sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
);
}
}
/**
* Decode POST data
*
* @param array $data
* @return array
*/
protected function decodeData($data): array
{
if (!\is_array($data)) {
return [];
}
// Decode JSON encoded fields and merge them to data.
if (isset($data['_json'])) {
$data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
unset($data['_json']);
}
return $data;
}
/**
* Recursively JSON decode POST data.
*
* @param array $data
* @return array
*/
protected function jsonDecode(array $data): array
{
foreach ($data as $key => &$value) {
if (\is_array($value)) {
$value = $this->jsonDecode($value);
} elseif (trim($value) === '') {
unset($data[$key]);
} else {
$value = json_decode($value, true);
if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
unset($data[$key]);
$this->setError("Badly encoded JSON data (for {$key}) was sent to the form");
}
}
}
return $data;
}
/**
* @return array
*/
protected function doSerialize(): array
{
$data = $this->data instanceof Data ? $this->data->toArray() : null;
return [
'name' => $this->name,
'id' => $this->id,
'uniqueid' => $this->uniqueid,
'submitted' => $this->submitted,
'status' => $this->status,
'message' => $this->message,
'messages' => $this->messages,
'data' => $data,
'files' => $this->files,
];
}
/**
* @param array $data
*/
protected function doUnserialize(array $data): void
{
$this->name = $data['name'];
$this->id = $data['id'];
$this->uniqueid = $data['uniqueid'];
$this->submitted = $data['submitted'] ?? false;
$this->status = $data['status'] ?? 'success';
$this->message = $data['message'] ?? null;
$this->messages = $data['messages'] ?? [];
$this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null;
$this->files = $data['files'] ?? [];
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Interfaces
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Interfaces;
use Grav\Framework\ContentBlock\ContentBlockInterface;
use Grav\Framework\ContentBlock\HtmlBlock;
/**
* Defines common interface to render any object.
*
* @used-by \Grav\Framework\Flex\FlexObject
* @since 1.6
*/
interface RenderInterface
{
/**
* Renders the object.
*
* @example $block = $object->render('custom', ['variable' => 'value']);
* @example {% render object layout 'custom' with { variable: 'value' } %}
*
* @param string|null $layout Layout to be used.
* @param array $context Extra context given to the renderer.
*
* @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output.
* @api
*/
public function render(string $layout = null, array $context = []);
}
@@ -0,0 +1,17 @@
<?php
/**
* @package Grav\Framework\Media
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Media\Interfaces;
/**
* Class implements media collection interface.
*/
interface MediaCollectionInterface extends \ArrayAccess, \Countable, \Iterator
{
}
@@ -0,0 +1,37 @@
<?php
/**
* @package Grav\Framework\Media
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Media\Interfaces;
/**
* Class implements media interface.
*/
interface MediaInterface
{
/**
* Gets the associated media collection.
*
* @return MediaCollectionInterface Collection of associated media.
*/
public function getMedia();
/**
* Get filesystem path to the associated media.
*
* @return string|null Media path or null if the object doesn't have media folder.
*/
public function getMediaFolder();
/**
* Get display order for the associated media.
*
* @return array Empty array means default ordering.
*/
public function getMediaOrder();
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Media
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Media\Interfaces;
use Grav\Common\Media\Interfaces\MediaInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* Interface MediaManipulationInterface
* @package Grav\Framework\Media\Interfaces
*/
interface MediaManipulationInterface extends MediaInterface
{
/**
* @param UploadedFileInterface $uploadedFile
*/
public function uploadMediaFile(UploadedFileInterface $uploadedFile) : void;
/**
* @param string $filename
*/
public function deleteMediaFile(string $filename) : void;
}
@@ -0,0 +1,17 @@
<?php
/**
* @package Grav\Framework\Media
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Media\Interfaces;
/**
* Class implements media object interface.
*/
interface MediaObjectInterface
{
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -37,8 +38,8 @@ trait NestedPropertyTrait
public function getNestedProperty($property, $default = null, $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
$path = explode($separator, $property) ?: [];
$offset = array_shift($path) ?? '';
if (!$this->hasProperty($offset)) {
return $default;
@@ -57,9 +58,9 @@ trait NestedPropertyTrait
$offset = array_shift($path);
if ((is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) {
if ((\is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) {
$current = $current[$offset];
} elseif (is_object($current) && isset($current->{$offset})) {
} elseif (\is_object($current) && isset($current->{$offset})) {
$current = $current->{$offset};
} else {
return $default;
@@ -80,8 +81,8 @@ trait NestedPropertyTrait
public function setNestedProperty($property, $value, $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
$path = explode($separator, $property) ?: [];
$offset = array_shift($path) ?? '';
if (!$path) {
$this->setProperty($offset, $value);
@@ -97,12 +98,12 @@ trait NestedPropertyTrait
// Handle arrays and scalars.
if ($current === null) {
$current = [$offset => []];
} elseif (is_array($current)) {
} elseif (\is_array($current)) {
if (!isset($current[$offset])) {
$current[$offset] = [];
}
} else {
throw new \RuntimeException('Cannot set nested property on non-array value');
throw new \RuntimeException("Cannot set nested property {$property} on non-array value");
}
$current = &$current[$offset];
@@ -122,8 +123,8 @@ trait NestedPropertyTrait
public function unsetNestedProperty($property, $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
$path = explode($separator, $property) ?: [];
$offset = array_shift($path) ?? '';
if (!$path) {
$this->unsetProperty($offset);
@@ -141,12 +142,12 @@ trait NestedPropertyTrait
if ($current === null) {
return $this;
}
if (is_array($current)) {
if (\is_array($current)) {
if (!isset($current[$offset])) {
return $this;
}
} else {
throw new \RuntimeException('Cannot set nested property on non-array value');
throw new \RuntimeException("Cannot unset nested property {$property} on non-array value");
}
$current = &$current[$offset];
@@ -159,7 +160,7 @@ trait NestedPropertyTrait
/**
* @param string $property Object property to be updated.
* @param string $default Default value.
* @param mixed $default Default value.
* @param string $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -16,11 +17,13 @@ use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\ArrayPropertyTrait;
/**
* Array Object class.
*
* @package Grav\Framework\Object
* Array Objects keep the data in private array property.
*/
class ArrayObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, ArrayPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
use ObjectTrait;
use ArrayPropertyTrait;
use NestedPropertyTrait;
use OverloadedPropertyTrait;
use NestedArrayAccessTrait;
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -29,7 +30,7 @@ trait ObjectCollectionTrait
{
$list = [];
foreach ($this->getIterator() as $key => $value) {
$list[$key] = is_object($value) ? clone $value : $value;
$list[$key] = \is_object($value) ? clone $value : $value;
}
return $this->createFrom($list);
@@ -45,7 +46,7 @@ trait ObjectCollectionTrait
/**
* @param string $property Object property to be matched.
* @return array Key/Value pairs of the properties.
* @return bool[] Key/Value pairs of the properties.
*/
public function doHasProperty($property)
{
@@ -62,7 +63,7 @@ trait ObjectCollectionTrait
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if not set.
* @return array Key/Value pairs of the properties.
* @return mixed[] Key/Value pairs of the properties.
*/
public function doGetProperty($property, $default = null)
{
@@ -78,7 +79,7 @@ trait ObjectCollectionTrait
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param mixed $value New value.
* @return $this
*/
public function doSetProperty($property, $value)
@@ -107,7 +108,7 @@ trait ObjectCollectionTrait
/**
* @param string $property Object property to be updated.
* @param string $default Default value.
* @param mixed $default Default value.
* @return $this
*/
public function doDefProperty($property, $default)
@@ -123,15 +124,19 @@ trait ObjectCollectionTrait
/**
* @param string $method Method name.
* @param array $arguments List of arguments passed to the function.
* @return array Return values.
* @return mixed[] Return values.
*/
public function call($method, array $arguments = [])
{
$list = [];
/**
* @var string|int $id
* @var ObjectInterface $element
*/
foreach ($this->getIterator() as $id => $element) {
$list[$id] = method_exists($element, $method)
? call_user_func_array([$element, $method], $arguments) : null;
$callable = method_exists($element, $method) ? [$element, $method] : null;
$list[$id] = $callable ? \call_user_func_array($callable, $arguments) : null;
}
return $list;
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -43,7 +44,7 @@ trait ObjectTrait
return $type . static::$type;
}
$class = get_class($this);
$class = \get_class($this);
return $type . strtolower(substr($class, strrpos($class, '\\') + 1));
}
@@ -52,12 +53,20 @@ trait ObjectTrait
*/
public function getKey()
{
return $this->_key ?: $this->getType() . '@' . spl_object_hash($this);
return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this);
}
/**
* @return bool
*/
public function hasKey()
{
return !empty($this->_key);
}
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
* @return bool|bool[] True if property has been defined (can be null).
*/
public function hasProperty($property)
{
@@ -67,7 +76,7 @@ trait ObjectTrait
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return mixed Property value.
* @return mixed|mixed[] Property value.
*/
public function getProperty($property, $default = null)
{
@@ -76,7 +85,7 @@ trait ObjectTrait
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param mixed $value New value.
* @return $this
*/
public function setProperty($property, $value)
@@ -139,7 +148,7 @@ trait ObjectTrait
*/
protected function doSerialize()
{
return $this->jsonSerialize();
return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()];
}
/**
@@ -162,7 +171,7 @@ trait ObjectTrait
*/
public function jsonSerialize()
{
return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()];
return $this->doSerialize();
}
/**
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -100,7 +101,7 @@ class ObjectExpressionVisitor extends ClosureExpressionVisitor
public static function sortByField($name, $orientation = 1, \Closure $next = null)
{
if (!$next) {
$next = function() {
$next = function($a, $b) {
return 0;
};
}
@@ -174,7 +175,7 @@ class ObjectExpressionVisitor extends ClosureExpressionVisitor
case Comparison::MEMBER_OF:
return function ($object) use ($field, $value) {
$fieldValues = static::getObjectFieldValue($object, $field);
if (!is_array($fieldValues)) {
if (!\is_array($fieldValues)) {
$fieldValues = iterator_to_array($fieldValues);
}
return \in_array($value, $fieldValues, true);
@@ -1,55 +1,56 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Interfaces;
/**
* Object Interface
* Common Interface for both Objects and Collections
* @package Grav\Framework\Object
*/
interface NestedObjectInterface extends ObjectInterface
{
/**
* @param string $property Object property name.
* @param string $separator Separator, defaults to '.'
* @return bool True if property has been defined (can be null).
* @param string $property Object property name.
* @param string|null $separator Separator, defaults to '.'
* @return bool|bool[] True if property has been defined (can be null).
*/
public function hasNestedProperty($property, $separator = null);
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @param string $separator Separator, defaults to '.'
* @return mixed Property value.
* @param string $property Object property to be fetched.
* @param mixed|null $default Default value if property has not been set.
* @param string|null $separator Separator, defaults to '.'
* @return mixed|mixed[] Property value.
*/
public function getNestedProperty($property, $default = null, $separator = null);
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param string $separator Separator, defaults to '.'
* @param string $property Object property to be updated.
* @param mixed $value New value.
* @param string|null $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
public function setNestedProperty($property, $value, $separator = null);
/**
* @param string $property Object property to be defined.
* @param string $default Default value.
* @param string $separator Separator, defaults to '.'
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @param string|null $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
public function defNestedProperty($property, $default, $separator = null);
/**
* @param string $property Object property to be unset.
* @param string $separator Separator, defaults to '.'
* @param string $property Object property to be unset.
* @param string|null $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -35,13 +36,6 @@ interface ObjectCollectionInterface extends CollectionInterface, Selectable, Obj
*/
public function getObjectKeys();
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if not set.
* @return array Property value.
*/
public function getProperty($property, $default = null);
/**
* @param string $name Method name.
* @param array $arguments List of arguments passed to the function.
@@ -64,4 +58,17 @@ interface ObjectCollectionInterface extends CollectionInterface, Selectable, Obj
* @return static[]
*/
public function collectionGroup($property);
/**
* @param array $ordering
* @return ObjectCollectionInterface
*/
public function orderBy(array $ordering);
/**
* @param int $start
* @param int|null $limit
* @return ObjectCollectionInterface
*/
public function limit($start, $limit = null);
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -25,34 +26,34 @@ interface ObjectInterface extends \Serializable, \JsonSerializable
public function getKey();
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
* @param string $property Object property name.
* @return bool|bool[] True if property has been defined (can be null).
*/
public function hasProperty($property);
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return mixed Property value.
* @param string $property Object property to be fetched.
* @param mixed|null $default Default value if property has not been set.
* @return mixed|mixed[] Property value.
*/
public function getProperty($property, $default = null);
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param string $property Object property to be updated.
* @param mixed $value New value.
* @return $this
*/
public function setProperty($property, $value);
/**
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @return $this
*/
public function defProperty($property, $default);
/**
* @param string $property Object property to be unset.
* @param string $property Object property to be unset.
* @return $this
*/
public function unsetProperty($property);
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -16,11 +17,16 @@ use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\LazyPropertyTrait;
/**
* Lazy Object class.
* Lazy Objects keep their data in both protected object properties and falls back to a stored array if property does
* not exist or is not initialized.
*
* @package Grav\Framework\Object
*/
class LazyObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, LazyPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
use ObjectTrait;
use LazyPropertyTrait;
use NestedPropertyTrait;
use OverloadedPropertyTrait;
use NestedArrayAccessTrait;
}
@@ -1,13 +1,15 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Framework\Collection\ArrayCollection;
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;
@@ -17,12 +19,12 @@ use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
/**
* Object Collection
* @package Grav\Framework\Object
* Class contains a collection of objects.
*/
class ObjectCollection extends ArrayCollection implements ObjectCollectionInterface, NestedObjectInterface
{
use ObjectCollectionTrait, NestedPropertyCollectionTrait {
use ObjectCollectionTrait;
use NestedPropertyCollectionTrait {
NestedPropertyCollectionTrait::group insteadof ObjectCollectionTrait;
}
@@ -35,7 +37,28 @@ class ObjectCollection extends ArrayCollection implements ObjectCollectionInterf
{
parent::__construct($this->setElements($elements));
$this->setKey($key);
$this->setKey($key ?? '');
}
/**
* @param array $ordering
* @return Collection|static
*/
public function orderBy(array $ordering)
{
$criteria = Criteria::create()->orderBy($ordering);
return $this->matching($criteria);
}
/**
* @param int $start
* @param int|null $limit
* @return static
*/
public function limit($start, $limit = null)
{
return $this->createFrom($this->slice($start, $limit));
}
/**
@@ -54,18 +77,24 @@ class ObjectCollection extends ArrayCollection implements ObjectCollectionInterf
if ($orderings = $criteria->getOrderings()) {
$next = null;
/**
* @var string $field
* @var string $ordering
*/
foreach (array_reverse($orderings) as $field => $ordering) {
$next = ObjectExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
$next = ObjectExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next);
}
uasort($filtered, $next);
if ($next) {
uasort($filtered, $next);
}
}
$offset = $criteria->getFirstResult();
$length = $criteria->getMaxResults();
if ($offset || $length) {
$filtered = array_slice($filtered, (int)$offset, $length);
$filtered = \array_slice($filtered, (int)$offset, $length);
}
return $this->createFrom($filtered);
@@ -0,0 +1,251 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use Doctrine\Common\Collections\Criteria;
use Grav\Framework\Collection\AbstractIndexCollection;
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
/**
* Keeps index of objects instead of collection of objects. This class allows you to keep a list of objects and load
* them on demand. The class can be used seemingly instead of ObjectCollection when the objects haven't been loaded yet.
*
* This is an abstract class and has some protected abstract methods to load objects which you need to implement in
* order to use the class.
*/
abstract class ObjectIndex extends AbstractIndexCollection implements ObjectCollectionInterface, NestedObjectInterface
{
/** @var string */
static protected $type;
/**
* @var string
*/
private $_key;
/**
* @param bool $prefix
* @return string
*/
public function getType($prefix = true)
{
$type = $prefix ? $this->getTypePrefix() : '';
if (static::$type) {
return $type . static::$type;
}
$class = \get_class($this);
return $type . strtolower(substr($class, strrpos($class, '\\') + 1));
}
/**
* @return string
*/
public function getKey()
{
return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this);
}
/**
* @param string $key
* @return $this
*/
public function setKey($key)
{
$this->_key = $key;
return $this;
}
/**
* @param string $property Object property name.
* @return array True if property has been defined (can be null).
*/
public function hasProperty($property)
{
return $this->__call('hasProperty', [$property]);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return array Property values.
*/
public function getProperty($property, $default = null)
{
return $this->__call('getProperty', [$property, $default]);
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @return ObjectCollectionInterface
*/
public function setProperty($property, $value)
{
return $this->__call('setProperty', [$property, $value]);
}
/**
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @return ObjectCollectionInterface
*/
public function defProperty($property, $default)
{
return $this->__call('defProperty', [$property, $default]);
}
/**
* @param string $property Object property to be unset.
* @return ObjectCollectionInterface
*/
public function unsetProperty($property)
{
return $this->__call('unsetProperty', [$property]);
}
/**
* @param string $property Object property name.
* @param string $separator Separator, defaults to '.'
* @return bool True if property has been defined (can be null).
*/
public function hasNestedProperty($property, $separator = null)
{
return $this->__call('hasNestedProperty', [$property, $separator]);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @param string $separator Separator, defaults to '.'
* @return mixed Property value.
*/
public function getNestedProperty($property, $default = null, $separator = null)
{
return $this->__call('getNestedProperty', [$property, $default, $separator]);
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param string $separator Separator, defaults to '.'
* @return ObjectCollectionInterface
*/
public function setNestedProperty($property, $value, $separator = null)
{
return $this->__call('setNestedProperty', [$property, $value, $separator]);
}
/**
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @param string $separator Separator, defaults to '.'
* @return ObjectCollectionInterface
*/
public function defNestedProperty($property, $default, $separator = null)
{
return $this->__call('defNestedProperty', [$property, $default, $separator]);
}
/**
* @param string $property Object property to be unset.
* @return ObjectCollectionInterface
*/
public function unsetNestedProperty($property, $separator = null)
{
return $this->__call('unsetNestedProperty', [$property, $separator]);
}
/**
* Create a copy from this collection by cloning all objects in the collection.
*
* @return static
*/
public function copy()
{
$list = [];
foreach ($this->getIterator() as $key => $value) {
$list[$key] = \is_object($value) ? clone $value : $value;
}
return $this->createFrom($list);
}
/**
* @return array
*/
public function getObjectKeys()
{
return $this->getKeys();
}
/**
* @param array $ordering
* @return ObjectCollectionInterface
*/
public function orderBy(array $ordering)
{
return $this->__call('orderBy', [$ordering]);
}
/**
* {@inheritDoc}
*/
public function call($method, array $arguments = [])
{
return $this->__call('call', [$method, $arguments]);
}
/**
* Group items in the collection by a field and return them as associated array.
*
* @param string $property
* @return array
*/
public function group($property)
{
return $this->__call('group', [$property]);
}
/**
* Group items in the collection by a field and return them as associated array of collections.
*
* @param string $property
* @return ObjectCollectionInterface[]
*/
public function collectionGroup($property)
{
return $this->__call('collectionGroup', [$property]);
}
/**
* {@inheritDoc}
*/
public function matching(Criteria $criteria)
{
/** @var ObjectCollectionInterface $collection */
$collection = $this->loadCollection($this->getEntries());
return $collection->matching($criteria);
}
abstract public function __call($name, $arguments);
/**
* @return string
*/
protected function getTypePrefix()
{
return '';
}
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -31,7 +32,7 @@ trait ArrayPropertyTrait
public function __construct(array $elements = [], $key = null)
{
$this->setElements($elements);
$this->setKey($key);
$this->setKey($key ?? '');
}
/**
@@ -94,7 +95,7 @@ trait ArrayPropertyTrait
*/
protected function getElements()
{
return $this->_elements;
return array_filter($this->_elements, function ($val) { return $val !== null; });
}
/**
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -37,7 +38,7 @@ trait ObjectPropertyTrait
{
$this->initObjectProperties();
$this->setElements($elements);
$this->setKey($key);
$this->setKey($key ?? '');
}
/**
@@ -110,7 +111,7 @@ trait ObjectPropertyTrait
if ($doCreate === true) {
$this->_definedProperties[$property] = true;
$this->{$property} = null;
} elseif (is_callable($doCreate)) {
} elseif (\is_callable($doCreate)) {
$this->_definedProperties[$property] = true;
$this->{$property} = $this->offsetLoad($property, $doCreate());
} else {
@@ -152,7 +153,7 @@ trait ObjectPropertyTrait
protected function initObjectProperties()
{
$this->_definedProperties = [];
foreach (get_object_vars($this) as $property => $value) {
foreach (\get_object_vars($this) as $property => $value) {
if ($property[0] !== '_') {
$this->_definedProperties[$property] = ($value !== null);
}
@@ -182,7 +183,10 @@ trait ObjectPropertyTrait
$elements = [];
foreach ($properties as $offset => $value) {
$elements[$offset] = $this->offsetSerialize($offset, $value);
$serialized = $this->offsetSerialize($offset, $value);
if ($serialized !== null) {
$elements[$offset] = $this->offsetSerialize($offset, $value);
}
}
return $elements;
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -16,11 +17,13 @@ use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\ObjectPropertyTrait;
/**
* Property Object class.
*
* @package Grav\Framework\Object
* Property Objects keep their data in protected object properties.
*/
class PropertyObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, ObjectPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
use ObjectTrait;
use ObjectPropertyTrait;
use NestedPropertyTrait;
use OverloadedPropertyTrait;
use NestedArrayAccessTrait;
}
@@ -0,0 +1,326 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination;
use Grav\Framework\Pagination\Interfaces\PaginationInterface;
use Grav\Framework\Route\Route;
class AbstractPagination implements PaginationInterface
{
/** @var Route Base rouse used for the pagination. */
protected $route;
/** @var int|null Current page. */
protected $page;
/** @var int|null The record number to start displaying from. */
protected $start;
/** @var int Number of records to display per page. */
protected $limit;
/** @var int Total number of records. */
protected $total;
/** @var array Pagination options */
protected $options;
/** @var bool View all flag. */
protected $viewAll;
/** @var int Total number of pages. */
protected $pages;
/** @var int Value pagination object begins at. */
protected $pagesStart;
/** @var int Value pagination object ends at .*/
protected $pagesStop;
/** @var array */
protected $defaultOptions = [
'type' => 'page',
'limit' => 10,
'display' => 5,
'opening' => 0,
'ending' => 0,
'url' => null
];
/** @var array */
private $items;
public function isEnabled(): bool
{
return $this->count() > 1;
}
public function getOptions(): array
{
return $this->options;
}
public function getRoute(): ?Route
{
return $this->route;
}
public function getTotalPages(): int
{
return $this->pages;
}
public function getPageNumber(): int
{
return $this->page;
}
public function getPrevNumber(int $count = 1): ?int
{
$page = $this->page - $count;
return $page >= 1 ? $page : null;
}
public function getNextNumber(int $count = 1): ?int
{
$page = $this->page + $count;
return $page <= $this->pages ? $page : null;
}
public function getPage(int $page, string $label = null): ?PaginationPage
{
if ($page < 1 || $page > $this->pages) {
return null;
}
$start = ($page - 1) * $this->limit;
if ($this->getOptions()['type'] === 'page') {
$name = 'page';
$offset = $page;
} else {
$name = 'start';
$offset = $start;
}
return new PaginationPage(
[
'label' => $label ?? (string)$page,
'number' => $page,
'offset_start' => $start,
'offset_end' => min($start + $this->limit, $this->total) - 1,
'enabled' => $page !== $this->page || $this->viewAll,
'active' => $page === $this->page,
'route' => $this->route->withGravParam($name, $offset)
]
);
}
public function getFirstPage(string $label = null, int $count = 0): ?PaginationPage
{
return $this->getPage(1 + $count, $label ?? $this->getOptions()['label_first'] ?? null);
}
public function getPrevPage(string $label = null, int $count = 1): ?PaginationPage
{
return $this->getPage($this->page - $count, $label ?? $this->getOptions()['label_prev'] ?? null);
}
public function getNextPage(string $label = null, int $count = 1): ?PaginationPage
{
return $this->getPage($this->page + $count, $label ?? $this->getOptions()['label_next'] ?? null);
}
public function getLastPage(string $label = null, int $count = 0): ?PaginationPage
{
return $this->getPage($this->pages - $count, $label ?? $this->getOptions()['label_last'] ?? null);
}
public function getStart(): int
{
return $this->start;
}
public function getLimit(): int
{
return $this->limit;
}
public function getTotal(): int
{
return $this->total;
}
public function count(): int
{
$this->loadItems();
return \count($this->items);
}
public function getIterator()
{
$this->loadItems();
return new \ArrayIterator($this->items);
}
public function getPages(): array
{
$this->loadItems();
return $this->items;
}
protected function loadItems()
{
$this->calculateRange();
// Make list like: 1 ... 4 5 6 ... 10
$range = range($this->pagesStart, $this->pagesStop);
//$range[] = 1;
//$range[] = $this->pages;
natsort($range);
$range = array_unique($range);
$this->items = [];
foreach ($range as $i) {
$this->items[$i] = $this->getPage($i);
}
}
protected function setRoute(Route $route)
{
$this->route = $route;
return $this;
}
protected function setOptions(array $options = null)
{
$this->options = $options ? array_merge($this->defaultOptions, $options) : $this->defaultOptions;
return $this;
}
protected function setPage(int $page = null)
{
$this->page = (int)max($page, 1);
$this->start = null;
return $this;
}
/**
* @param int $start
* @return $this
*/
protected function setStart(int $start = null)
{
$this->start = (int)max($start, 0);
$this->page = null;
return $this;
}
/**
* @param int|null $limit
* @return $this
*/
protected function setLimit(int $limit = null)
{
$this->limit = (int)max($limit ?? $this->getOptions()['limit'], 0);
// No limit, display all records in a single page.
$this->viewAll = !$limit;
return $this;
}
/**
* @param int $total
* @return $this
*/
protected function setTotal(int $total)
{
$this->total = (int)max($total, 0);
return $this;
}
protected function initialize(Route $route, int $total, int $pos = null, int $limit = null, array $options = null)
{
$this->setRoute($route);
$this->setOptions($options);
$this->setTotal($total);
if ($this->getOptions()['type'] === 'start') {
$this->setStart($pos);
} else {
$this->setPage($pos);
}
$this->setLimit($limit);
$this->calculateLimits();
}
protected function calculateLimits()
{
$limit = $this->limit;
$total = $this->total;
if (!$limit || $limit > $total) {
// All records fit into a single page.
$this->start = 0;
$this->page = 1;
$this->pages = 1;
return;
}
if (null === $this->start) {
// If we are using page, convert it to start.
$this->start = (int)(($this->page - 1) * $limit);
}
if ($this->start > $total - $limit) {
// If start is greater than total count (i.e. we are asked to display records that don't exist)
// then set start to display the last natural page of results.
$this->start = (int)max(0, (ceil($total / $limit) - 1) * $limit);
}
// Set the total pages and current page values.
$this->page = (int)ceil(($this->start + 1) / $limit);
$this->pages = (int)ceil($total / $limit);
}
protected function calculateRange()
{
$options = $this->getOptions();
$displayed = $options['display'];
$opening = $options['opening'];
$ending = $options['ending'];
// Set the pagination iteration loop values.
$this->pagesStart = $this->page - (int)($displayed / 2);
if ($this->pagesStart < 1 + $opening) {
$this->pagesStart = 1 + $opening;
}
if ($this->pagesStart + $displayed - $opening > $this->pages) {
$this->pagesStop = $this->pages;
if ($this->pages < $displayed) {
$this->pagesStart = 1 + $opening;
} else {
$this->pagesStart = $this->pages - $displayed + 1 + $opening;
}
} else {
$this->pagesStop = (int)max(1, $this->pagesStart + $displayed - 1 - $ending);
}
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination;
use Grav\Framework\Pagination\Interfaces\PaginationPageInterface;
abstract class AbstractPaginationPage implements PaginationPageInterface
{
/** @var array */
protected $options;
public function isActive(): bool
{
return $this->options['active'] ?? false;
}
public function isEnabled(): bool
{
return $this->options['enabled'] ?? false;
}
public function getOptions(): array
{
return $this->options ?? [];
}
public function getNumber(): ?int
{
return $this->options['number'] ?? null;
}
public function getLabel(): string
{
return $this->options['label'] ?? (string)$this->getNumber();
}
public function getUrl(): ?string
{
return $this->options['route'] ? (string)$this->options['route']->getUri() : null;
}
protected function setOptions(array $options): void
{
$this->options = $options;
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination\Interfaces;
use Grav\Framework\Pagination\PaginationPage;
interface PaginationInterface extends \Countable, \IteratorAggregate
{
public function getTotalPages(): int;
public function getPageNumber(): int;
public function getPrevNumber(int $count = 1): ?int;
public function getNextNumber(int $count = 1): ?int;
public function getStart(): int;
public function getLimit(): int;
public function getTotal(): int;
public function count(): int;
public function getOptions(): array;
public function getPage(int $page, string $label = null): ?PaginationPage;
public function getFirstPage(string $label = null, int $count = 0): ?PaginationPage;
public function getPrevPage(string $label = null, int $count = 1): ?PaginationPage;
public function getNextPage(string $label = null, int $count = 1): ?PaginationPage;
public function getLastPage(string $label = null, int $count = 0): ?PaginationPage;
}
@@ -0,0 +1,25 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination\Interfaces;
interface PaginationPageInterface
{
public function isActive(): bool;
public function isEnabled(): bool;
public function getOptions(): array;
public function getNumber(): ?int;
public function getLabel(): string;
public function getUrl(): ?string;
}
@@ -0,0 +1,20 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination;
use Grav\Framework\Route\Route;
class Pagination extends AbstractPagination
{
public function __construct(Route $route, int $total, int $pos = null, int $limit = null, array $options = null)
{
$this->initialize($route, $total, $pos, $limit, $options);
}
}
@@ -0,0 +1,18 @@
<?php
/**
* @package Grav\Framework\Pagination
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Pagination;
class PaginationPage extends AbstractPaginationPage
{
public function __construct(array $options = [])
{
$this->setOptions($options);
}
}
@@ -1,8 +1,9 @@
<?php
/**
* @package Grav\Framework\Psr7
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
@@ -15,6 +16,7 @@ use Psr\Http\Message\UriInterface;
* Bare minimum PSR7 implementation.
*
* @package Grav\Framework\Uri\Psr7
* @deprecated 1.6 Using message PSR-7 decorators instead.
*/
abstract class AbstractUri implements UriInterface
{
@@ -156,10 +158,10 @@ abstract class AbstractUri implements UriInterface
* @inheritdoc
* @throws \InvalidArgumentException
*/
public function withUserInfo($user, $password = '')
public function withUserInfo($user, $password = null)
{
$user = UriPartsFilter::filterUserInfo($user);
$password = UriPartsFilter::filterUserInfo($password);
$password = UriPartsFilter::filterUserInfo($password ?? '');
if ($this->user === $user && $this->password === $password) {
return $this;
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* @package Grav\Framework\Psr7
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Psr7;
use Grav\Framework\Psr7\Traits\RequestDecoratorTrait;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
class Request implements RequestInterface
{
use RequestDecoratorTrait;
/**
* @param string $method HTTP method
* @param string|UriInterface $uri URI
* @param array $headers Request headers
* @param string|null|resource|StreamInterface $body Request body
* @param string $version Protocol version
*/
public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
{
$this->message = new \Nyholm\Psr7\Request($method, $uri, $headers, $body, $version);
}
}

Some files were not shown because too many files have changed in this diff Show More