first commit

This commit is contained in:
2019-03-28 17:57:56 +01:00
commit b0e25fd66f
561 changed files with 56803 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache trait for PSR-16 compatible "Simple Cache" implementation
* @package Grav\Framework\Cache
*/
abstract class AbstractCache implements CacheInterface
{
use CacheTrait;
/**
* @param string $namespace
* @param null|int|\DateInterval $defaultLifetime
* @throws InvalidArgumentException
*/
public function __construct($namespace = '', $defaultLifetime = null)
{
$this->init($namespace, $defaultLifetime);
}
}

View File

@@ -0,0 +1,197 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using chained cache adapters.
*
* @package Grav\Framework\Cache
*/
class ChainCache extends AbstractCache
{
/**
* @var array|CacheInterface[]
*/
protected $caches;
/**
* @var int
*/
protected $count;
/**
* Chain Cache constructor.
* @param array $caches
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function __construct(array $caches, $defaultLifetime = null)
{
parent::__construct('', $defaultLifetime);
if (!$caches) {
throw new InvalidArgumentException('At least one cache must be specified');
}
foreach ($caches as $cache) {
if (!$cache instanceof CacheInterface) {
throw new InvalidArgumentException(
sprintf(
"The class '%s' does not implement the '%s' interface",
get_class($cache),
CacheInterface::class
)
);
}
}
$this->caches = array_values($caches);
$this->count = count($caches);
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
foreach ($this->caches as $i => $cache) {
$value = $cache->doGet($key, $miss);
if ($value !== $miss) {
while (--$i >= 0) {
// Update all the previous caches with missing value.
$this->caches[$i]->doSet($key, $value, $this->getDefaultLifetime());
}
return $value;
}
}
return $miss;
}
/**
* @inheritdoc
*/
public function doSet($key, $value, $ttl)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doSet($key, $value, $ttl) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doDelete($key) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doClear()
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doClear() && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doGetMultiple($keys, $miss)
{
$list = [];
foreach ($this->caches as $i => $cache) {
$list[$i] = $cache->doGetMultiple($keys, $miss);
$keys = array_diff_key($keys, $list[$i]);
if (!$keys) {
break;
}
}
$values = [];
// Update all the previous caches with missing values.
foreach (array_reverse($list) as $i => $items) {
$values += $items;
if ($i && $values) {
$this->caches[$i-1]->doSetMultiple($values, $this->getDefaultLifetime());
}
}
return $values;
}
/**
* @inheritdoc
*/
public function doSetMultiple($values, $ttl)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doSetMultiple($values, $ttl) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doDeleteMultiple($keys)
{
$success = true;
$i = $this->count;
while ($i--) {
$success = $this->caches[$i]->doDeleteMultiple($keys) && $success;
}
return $success;
}
/**
* @inheritdoc
*/
public function doHas($key)
{
foreach ($this->caches as $cache) {
if ($cache->doHas($key)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Doctrine\Common\Cache\CacheProvider;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using Doctrine Cache backend.
* @package Grav\Framework\Cache
*/
class DoctrineCache extends AbstractCache
{
/**
* @var CacheProvider
*/
protected $driver;
/**
* Doctrine Cache constructor.
*
* @param CacheProvider $doctrineCache
* @param string $namespace
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function __construct(CacheProvider $doctrineCache, $namespace = '', $defaultLifetime = null)
{
// Do not use $namespace or $defaultLifetime directly, store them with constructor and fetch with methods.
parent::__construct($namespace, $defaultLifetime);
// Set namespace to Doctrine Cache provider if it was given.
$namespace = $this->getNamespace();
$namespace && $doctrineCache->setNamespace($namespace);
$this->driver = $doctrineCache;
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
$value = $this->driver->fetch($key);
// Doctrine cache does not differentiate between no result and cached 'false'. Make sure that we do.
return $value !== false || $this->driver->contains($key) ? $value : $miss;
}
/**
* @inheritdoc
*/
public function doSet($key, $value, $ttl)
{
return $this->driver->save($key, $value, (int) $ttl);
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
return $this->driver->delete($key);
}
/**
* @inheritdoc
*/
public function doClear()
{
return $this->driver->deleteAll();
}
/**
* @inheritdoc
*/
public function doGetMultiple($keys, $miss)
{
return $this->driver->fetchMultiple($keys);
}
/**
* @inheritdoc
*/
public function doSetMultiple($values, $ttl)
{
return $this->driver->saveMultiple($values, (int) $ttl);
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\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);
}
/**
* @inheritdoc
*/
public function doHas($key)
{
return $this->driver->contains($key);
}
}

View File

@@ -0,0 +1,210 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
use Grav\Framework\Cache\Exception\CacheException;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using file backend.
*
* Defaults to 1 year TTL. Does not support unlimited TTL.
*
* @package Grav\Framework\Cache
*/
class FileCache extends AbstractCache
{
private $directory;
private $tmp;
/**
* @inheritdoc
*/
public function __construct($namespace = '', $defaultLifetime = null, $folder = null)
{
parent::__construct($namespace, $defaultLifetime ?: 31557600); // = 1 year
$this->initFileCache($namespace, $folder ?? '');
}
/**
* @inheritdoc
*/
public function doGet($key, $miss)
{
$now = time();
$file = $this->getFile($key);
if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
return $miss;
}
if ($now >= (int) $expiresAt = fgets($h)) {
fclose($h);
@unlink($file);
} else {
$i = rawurldecode(rtrim(fgets($h)));
$value = stream_get_contents($h);
fclose($h);
if ($i === $key) {
return unserialize($value);
}
}
return $miss;
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\CacheException
*/
public function doSet($key, $value, $ttl)
{
$expiresAt = time() + (int)$ttl;
$result = $this->write(
$this->getFile($key, true),
$expiresAt . "\n" . rawurlencode($key) . "\n" . serialize($value),
$expiresAt
);
if (!$result && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
}
return $result;
}
/**
* @inheritdoc
*/
public function doDelete($key)
{
$file = $this->getFile($key);
return (!file_exists($file) || @unlink($file) || !file_exists($file));
}
/**
* @inheritdoc
*/
public function doClear()
{
$result = true;
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $file) {
$result = ($file->isDir() || @unlink($file) || !file_exists($file)) && $result;
}
return $result;
}
/**
* @inheritdoc
*/
public function doHas($key)
{
$file = $this->getFile($key);
return file_exists($file) && (@filemtime($file) > time() || $this->doGet($key, null));
}
/**
* @param string $key
* @param bool $mkdir
* @return string
*/
protected function getFile($key, $mkdir = false)
{
$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);
}
return $dir . substr($hash, 2, 20);
}
/**
* @param string $namespace
* @param string $directory
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
*/
protected function initFileCache($namespace, $directory)
{
if (!isset($directory[0])) {
$directory = sys_get_temp_dir() . '/grav-cache';
} else {
$directory = realpath($directory) ?: $directory;
}
if (isset($namespace[0])) {
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
}
$directory .= DIRECTORY_SEPARATOR . $namespace;
}
$this->mkdir($directory);
$directory .= DIRECTORY_SEPARATOR;
// On Windows the whole path is limited to 258 chars
if ('\\' === DIRECTORY_SEPARATOR && strlen($directory) > 234) {
throw new InvalidArgumentException(sprintf('Cache folder is too long (%s)', $directory));
}
$this->directory = $directory;
}
/**
* @param string $file
* @param string $data
* @param int|null $expiresAt
* @return bool
*/
private function write($file, $data, $expiresAt = null)
{
set_error_handler(__CLASS__.'::throwError');
try {
if ($this->tmp === null) {
$this->tmp = $this->directory . uniqid('', true);
}
file_put_contents($this->tmp, $data);
if ($expiresAt !== null) {
touch($this->tmp, $expiresAt);
}
return rename($this->tmp, $file);
} finally {
restore_error_handler();
}
}
/**
* @internal
* @throws \ErrorException
*/
public static function throwError($type, $message, $file, $line)
{
throw new \ErrorException($message, 0, $type, $file, $line);
}
public function __destruct()
{
if ($this->tmp !== null && file_exists($this->tmp)) {
unlink($this->tmp);
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using in memory backend.
*
* Memory backend does not use namespace or default ttl as the cache is unique to each cache object and request.
*
* @package Grav\Framework\Cache
*/
class MemoryCache extends AbstractCache
{
/**
* @var array
*/
protected $cache = [];
public function doGet($key, $miss)
{
if (!array_key_exists($key, $this->cache)) {
return $miss;
}
return $this->cache[$key];
}
public function doSet($key, $value, $ttl)
{
$this->cache[$key] = $value;
return true;
}
public function doDelete($key)
{
unset($this->cache[$key]);
return true;
}
public function doClear()
{
$this->cache = [];
return true;
}
public function doHas($key)
{
return array_key_exists($key, $this->cache);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Adapter;
use Grav\Framework\Cache\AbstractCache;
/**
* Cache class for PSR-16 compatible "Simple Cache" implementation using session backend.
*
* @package Grav\Framework\Cache
*/
class SessionCache extends AbstractCache
{
const VALUE = 0;
const LIFETIME = 1;
public function doGet($key, $miss)
{
$stored = $this->doGetStored($key);
return $stored ? $stored[self::VALUE] : $miss;
}
public function doSet($key, $value, $ttl)
{
$stored = [self::VALUE => $value];
if (null !== $ttl) {
$stored[self::LIFETIME] = time() + $ttl;
}
$_SESSION[$this->getNamespace()][$key] = $stored;
return true;
}
public function doDelete($key)
{
unset($_SESSION[$this->getNamespace()][$key]);
return true;
}
public function doClear()
{
unset($_SESSION[$this->getNamespace()]);
return true;
}
public function doHas($key)
{
return $this->doGetStored($key) !== null;
}
public function getNamespace()
{
return 'cache-' . parent::getNamespace();
}
protected function doGetStored($key)
{
$stored = isset($_SESSION[$this->getNamespace()][$key]) ? $_SESSION[$this->getNamespace()][$key] : null;
if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) {
unset($_SESSION[$this->getNamespace()][$key]);
$stored = null;
}
return $stored ?: null;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache;
use Psr\SimpleCache\CacheInterface as SimpleCacheInterface;
/**
* PSR-16 compatible "Simple Cache" interface.
* @package Grav\Framework\Object\Storage
*/
interface CacheInterface extends SimpleCacheInterface
{
public function doGet($key, $miss);
public function doSet($key, $value, $ttl);
public function doDelete($key);
public function doClear();
public function doGetMultiple($keys, $miss);
public function doSetMultiple($values, $ttl);
public function doDeleteMultiple($keys);
public function doHas($key);
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache;
use Grav\Framework\Cache\Exception\InvalidArgumentException;
/**
* Cache trait for PSR-16 compatible "Simple Cache" implementation
* @package Grav\Framework\Cache
*/
trait CacheTrait
{
/** @var string */
private $namespace = '';
/** @var int|null */
private $defaultLifetime = null;
/** @var \stdClass */
private $miss;
/** @var bool */
private $validation = true;
/**
* Always call from constructor.
*
* @param string $namespace
* @param null|int|\DateInterval $defaultLifetime
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function init($namespace = '', $defaultLifetime = null)
{
$this->namespace = (string) $namespace;
$this->defaultLifetime = $this->convertTtl($defaultLifetime);
$this->miss = new \stdClass;
}
/**
* @param $validation
*/
public function setValidation($validation)
{
$this->validation = (bool) $validation;
}
/**
* @return string
*/
protected function getNamespace()
{
return $this->namespace;
}
/**
* @return int|null
*/
protected function getDefaultLifetime()
{
return $this->defaultLifetime;
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function get($key, $default = null)
{
$this->validateKey($key);
$value = $this->doGet($key, $this->miss);
return $value !== $this->miss ? $value : $default;
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function set($key, $value, $ttl = null)
{
$this->validateKey($key);
$ttl = $this->convertTtl($ttl);
// If a negative or zero TTL is provided, the item MUST be deleted from the cache.
return null !== $ttl && $ttl <= 0 ? $this->doDelete($key) : $this->doSet($key, $value, $ttl);
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function delete($key)
{
$this->validateKey($key);
return $this->doDelete($key);
}
/**
* @inheritdoc
*/
public function clear()
{
return $this->doClear();
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function getMultiple($keys, $default = null)
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} 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)
)
);
}
if (empty($keys)) {
return [];
}
$this->validateKeys($keys);
$keys = array_unique($keys);
$keys = array_combine($keys, $keys);
$list = $this->doGetMultiple($keys, $this->miss);
// Make sure that values are returned in the same order as the keys were given.
$values = [];
foreach ($keys as $key) {
if (!array_key_exists($key, $list) || $list[$key] === $this->miss) {
$values[$key] = $default;
} else {
$values[$key] = $list[$key];
}
}
return $values;
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function setMultiple($values, $ttl = null)
{
if ($values instanceof \Traversable) {
$values = iterator_to_array($values, true);
} elseif (!is_array($values)) {
throw new InvalidArgumentException(
sprintf(
'Cache values must be array or Traversable, "%s" given',
is_object($values) ? get_class($values) : gettype($values)
)
);
}
$keys = array_keys($values);
if (empty($keys)) {
return true;
}
$this->validateKeys($keys);
$ttl = $this->convertTtl($ttl);
// If a negative or zero TTL is provided, the item MUST be deleted from the cache.
return null !== $ttl && $ttl <= 0 ? $this->doDeleteMultiple($keys) : $this->doSetMultiple($values, $ttl);
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function deleteMultiple($keys)
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} 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)
)
);
}
if (empty($keys)) {
return true;
}
$this->validateKeys($keys);
return $this->doDeleteMultiple($keys);
}
/**
* @inheritdoc
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function has($key)
{
$this->validateKey($key);
return $this->doHas($key);
}
abstract public function doGet($key, $miss);
abstract public function doSet($key, $value, $ttl);
abstract public function doDelete($key);
abstract public function doClear();
/**
* @param array $keys
* @param mixed $miss
* @return array
*/
public function doGetMultiple($keys, $miss)
{
$results = [];
foreach ($keys as $key) {
$value = $this->doGet($key, $miss);
if ($value !== $miss) {
$results[$key] = $value;
}
}
return $results;
}
/**
* @param array $values
* @param int $ttl
* @return bool
*/
public function doSetMultiple($values, $ttl)
{
$success = true;
foreach ($values as $key => $value) {
$success = $this->doSet($key, $value, $ttl) && $success;
}
return $success;
}
/**
* @param array $keys
* @return bool
*/
public function doDeleteMultiple($keys)
{
$success = true;
foreach ($keys as $key) {
$success = $this->doDelete($key) && $success;
}
return $success;
}
abstract public function doHas($key);
/**
* @param string $key
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function validateKey($key)
{
if (!is_string($key)) {
throw new InvalidArgumentException(
sprintf(
'Cache key must be string, "%s" given',
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) {
throw new InvalidArgumentException(
sprintf('Cache key length must be less than 65 characters, key had %s characters', strlen($key))
);
}
if (strpbrk($key, '{}()/\@:') !== false) {
throw new InvalidArgumentException(
sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key)
);
}
}
/**
* @param array $keys
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function validateKeys($keys)
{
if (!$this->validation) {
return;
}
foreach ($keys as $key) {
$this->validateKey($key);
}
}
/**
* @param null|int|\DateInterval $ttl
* @return int|null
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
protected function convertTtl($ttl)
{
if ($ttl === null) {
return $this->getDefaultLifetime();
}
if (is_int($ttl)) {
return $ttl;
}
if ($ttl instanceof \DateInterval) {
$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)
)
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Exception;
use Psr\SimpleCache\CacheException as SimpleCacheException;
/**
* CacheException class for PSR-16 compatible "Simple Cache" implementation.
* @package Grav\Framework\Cache\Exception
*/
class CacheException extends \Exception implements SimpleCacheException
{
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Grav\Framework\Cache
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Cache\Exception;
use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInvalidArgumentException;
/**
* InvalidArgumentException class for PSR-16 compatible "Simple Cache" implementation.
* @package Grav\Framework\Cache\Exception
*/
class InvalidArgumentException extends \InvalidArgumentException implements SimpleCacheInvalidArgumentException
{
}

View File

@@ -0,0 +1,227 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\RecursiveUniformResourceIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Collection of objects stored into a filesystem.
*
* @package Grav\Framework\Collection
*/
class AbstractFileCollection extends AbstractLazyCollection implements FileCollectionInterface
{
/**
* @var string
*/
protected $path;
/**
* @var \RecursiveDirectoryIterator|RecursiveUniformResourceIterator
*/
protected $iterator;
/**
* @var callable
*/
protected $createObjectFunction;
/**
* @var callable
*/
protected $filterFunction;
/**
* @var int
*/
protected $flags;
/**
* @var int
*/
protected $nestingLimit;
/**
* @param string $path
*/
protected function __construct($path)
{
$this->path = $path;
$this->flags = self::INCLUDE_FILES | self::INCLUDE_FOLDERS;
$this->nestingLimit = 0;
$this->createObjectFunction = [$this, 'createObject'];
$this->setIterator();
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @param Criteria $criteria
* @return ArrayCollection
* @todo Implement lazy matching
*/
public function matching(Criteria $criteria)
{
$expr = $criteria->getWhereExpression();
$oldFilter = $this->filterFunction;
if ($expr) {
$visitor = new ClosureExpressionVisitor();
$filter = $visitor->dispatch($expr);
$this->addFilter($filter);
}
$filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit);
$this->filterFunction = $oldFilter;
if ($orderings = $criteria->getOrderings()) {
$next = null;
foreach (array_reverse($orderings) as $field => $ordering) {
$next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
}
uasort($filtered, $next);
} else {
ksort($filtered);
}
$offset = $criteria->getFirstResult();
$length = $criteria->getMaxResults();
if ($offset || $length) {
$filtered = array_slice($filtered, (int)$offset, $length);
}
return new ArrayCollection($filtered);
}
protected function setIterator()
{
$iteratorFlags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
+ \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
if (strpos($this->path, '://')) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$this->iterator = $locator->getRecursiveIterator($this->path, $iteratorFlags);
} else {
$this->iterator = new \RecursiveDirectoryIterator($this->path, $iteratorFlags);
}
}
/**
* @param callable $filterFunction
* @return $this
*/
protected function addFilter(callable $filterFunction)
{
if ($this->filterFunction) {
$oldFilterFunction = $this->filterFunction;
$this->filterFunction = function ($expr) use ($oldFilterFunction, $filterFunction) {
return $oldFilterFunction($expr) && $filterFunction($expr);
};
} else {
$this->filterFunction = $filterFunction;
}
return $this;
}
/**
* {@inheritDoc}
*/
protected function doInitialize()
{
$filtered = $this->doInitializeByIterator($this->iterator, $this->nestingLimit);
ksort($filtered);
$this->collection = new ArrayCollection($filtered);
}
protected function doInitializeByIterator(\SeekableIterator $iterator, $nestingLimit)
{
$children = [];
$objects = [];
$filter = $this->filterFunction;
$objectFunction = $this->createObjectFunction;
/** @var \RecursiveDirectoryIterator $file */
foreach ($iterator as $file) {
// Skip files if they shouldn't be included.
if (!($this->flags & static::INCLUDE_FILES) && $file->isFile()) {
continue;
}
// Apply main filter.
if ($filter && !$filter($file)) {
continue;
}
// Include children if the recursive flag is set.
if (($this->flags & static::RECURSIVE) && $nestingLimit > 0 && $file->hasChildren()) {
$children[] = $file->getChildren();
}
// Skip folders if they shouldn't be included.
if (!($this->flags & static::INCLUDE_FOLDERS) && $file->isDir()) {
continue;
}
$object = $objectFunction($file);
$objects[$object->key] = $object;
}
if ($children) {
$objects += $this->doInitializeChildren($children, $nestingLimit - 1);
}
return $objects;
}
/**
* @param \RecursiveDirectoryIterator[] $children
* @return array
*/
protected function doInitializeChildren(array $children, $nestingLimit)
{
$objects = [];
foreach ($children as $iterator) {
$objects += $this->doInitializeByIterator($iterator, $nestingLimit);
}
return $objects;
}
/**
* @param \RecursiveDirectoryIterator $file
* @return object
*/
protected function createObject($file)
{
return (object) [
'key' => $file->getSubPathname(),
'type' => $file->isDir() ? 'folder' : 'file:' . $file->getExtension(),
'url' => method_exists($file, 'getUrl') ? $file->getUrl() : null,
'pathname' => $file->getPathname(),
'mtime' => $file->getMTime()
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use Doctrine\Common\Collections\AbstractLazyCollection as BaseAbstractLazyCollection;
/**
* General JSON serializable collection.
*
* @package Grav\Framework\Collection
*/
abstract class AbstractLazyCollection extends BaseAbstractLazyCollection implements CollectionInterface
{
/**
* The backed collection to use
*
* @var ArrayCollection
*/
protected $collection;
/**
* {@inheritDoc}
*/
public function reverse()
{
$this->initialize();
return $this->collection->reverse();
}
/**
* {@inheritDoc}
*/
public function shuffle()
{
$this->initialize();
return $this->collection->shuffle();
}
/**
* {@inheritDoc}
*/
public function chunk($size)
{
$this->initialize();
return $this->collection->chunk($size);
}
/**
* {@inheritDoc}
*/
public function jsonSerialize()
{
$this->initialize();
return $this->collection->jsonSerialize();
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use Doctrine\Common\Collections\ArrayCollection as BaseArrayCollection;
/**
* General JSON serializable collection.
*
* @package Grav\Framework\Collection
*/
class ArrayCollection extends BaseArrayCollection implements CollectionInterface
{
/**
* Reverse the order of the items.
*
* @return static
*/
public function reverse()
{
return $this->createFrom(array_reverse($this->toArray()));
}
/**
* Shuffle items.
*
* @return static
*/
public function shuffle()
{
$keys = $this->getKeys();
shuffle($keys);
return $this->createFrom(array_replace(array_flip($keys), $this->toArray()));
}
/**
* Split collection into chunks.
*
* @param int $size Size of each chunk.
* @return array
*/
public function chunk($size)
{
return array_chunk($this->toArray(), $size, true);
}
/**
* Implementes JsonSerializable interface.
*
* @return array
*/
public function jsonSerialize()
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use Doctrine\Common\Collections\Collection;
/**
* Collection Interface.
*
* @package Grav\Framework\Collection
*/
interface CollectionInterface extends Collection, \JsonSerializable
{
/**
* Reverse the order of the items.
*
* @return static
*/
public function reverse();
/**
* Shuffle items.
*
* @return static
*/
public function shuffle();
/**
* Split collection into chunks.
*
* @param int $size Size of each chunk.
* @return array
*/
public function chunk($size);
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
/**
* Collection of objects stored into a filesystem.
*
* @package Grav\Framework\Collection
*/
class FileCollection extends AbstractFileCollection
{
/**
* @param string $path
* @param int $flags
*/
public function __construct($path, $flags = null)
{
parent::__construct($path);
$this->flags = (int)($flags ?: self::INCLUDE_FILES | self::INCLUDE_FOLDERS | self::RECURSIVE);
$this->setIterator();
$this->setFilter();
$this->setObjectBuilder();
$this->setNestingLimit();
}
/**
* @return int
*/
public function getFlags()
{
return $this->flags;
}
/**
* @return int
*/
public function getNestingLimit()
{
return $this->nestingLimit;
}
/**
* @param int $limit
* @return $this
*/
public function setNestingLimit($limit = 99)
{
$this->nestingLimit = (int) $limit;
return $this;
}
/**
* @param callable|null $filterFunction
* @return $this
*/
public function setFilter(callable $filterFunction = null)
{
$this->filterFunction = $filterFunction;
return $this;
}
/**
* @param callable $filterFunction
* @return $this
*/
public function addFilter(callable $filterFunction)
{
parent::addFilter($filterFunction);
return $this;
}
/**
* @param callable|null $objectFunction
* @return $this
*/
public function setObjectBuilder(callable $objectFunction = null)
{
$this->createObjectFunction = $objectFunction ?: [$this, 'createObject'];
return $this;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Collection;
use Doctrine\Common\Collections\Selectable;
/**
* Collection of objects stored into a filesystem.
*
* @package Grav\Framework\Collection
*/
interface FileCollectionInterface extends CollectionInterface, Selectable
{
const INCLUDE_FILES = 1;
const INCLUDE_FOLDERS = 2;
const RECURSIVE = 4;
/**
* @return string
*/
public function getPath();
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\ContentBlock;
/**
* Class to create nested blocks of content.
*
* $innerBlock = ContentBlock::create();
* $innerBlock->setContent('my inner content');
* $outerBlock = ContentBlock::create();
* $outerBlock->setContent(sprintf('Inside my outer block I have %s.', $innerBlock->getToken()));
* $outerBlock->addBlock($innerBlock);
* echo $outerBlock;
*
* @package Grav\Framework\ContentBlock
*/
class ContentBlock implements ContentBlockInterface
{
protected $version = 1;
protected $id;
protected $tokenTemplate = '@@BLOCK-%s@@';
protected $content = '';
protected $blocks = [];
protected $checksum;
/**
* @param string $id
* @return static
*/
public static function create($id = null)
{
return new static($id);
}
/**
* @param array $serialized
* @return ContentBlockInterface
* @throws \InvalidArgumentException
*/
public static function fromArray(array $serialized)
{
try {
$type = isset($serialized['_type']) ? $serialized['_type'] : null;
$id = isset($serialized['id']) ? $serialized['id'] : null;
if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) {
throw new \InvalidArgumentException('Bad data');
}
/** @var ContentBlockInterface $instance */
$instance = new $type($id);
$instance->build($serialized);
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
}
return $instance;
}
/**
* Block constructor.
*
* @param string $id
*/
public function __construct($id = null)
{
$this->id = $id ? (string) $id : $this->generateId();
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getToken()
{
return sprintf($this->tokenTemplate, $this->getId());
}
/**
* @return array
*/
public function toArray()
{
$blocks = [];
/**
* @var string $id
* @var ContentBlockInterface $block
*/
foreach ($this->blocks as $block) {
$blocks[$block->getId()] = $block->toArray();
}
$array = [
'_type' => get_class($this),
'_version' => $this->version,
'id' => $this->id
];
if ($this->checksum) {
$array['checksum'] = $this->checksum;
}
if ($this->content) {
$array['content'] = $this->content;
}
if ($blocks) {
$array['blocks'] = $blocks;
}
return $array;
}
/**
* @return string
*/
public function toString()
{
if (!$this->blocks) {
return (string) $this->content;
}
$tokens = [];
$replacements = [];
foreach ($this->blocks as $block) {
$tokens[] = $block->getToken();
$replacements[] = $block->toString();
}
return str_replace($tokens, $replacements, (string) $this->content);
}
/**
* @return string
*/
public function __toString()
{
try {
return $this->toString();
} catch (\Exception $e) {
return sprintf('Error while rendering block: %s', $e->getMessage());
}
}
/**
* @param array $serialized
* @throws \RuntimeException
*/
public function build(array $serialized)
{
$this->checkVersion($serialized);
$this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
$this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null;
if (isset($serialized['content'])) {
$this->setContent($serialized['content']);
}
$blocks = isset($serialized['blocks']) ? (array) $serialized['blocks'] : [];
foreach ($blocks as $block) {
$this->addBlock(self::fromArray($block));
}
}
/**
* @param string $checksum
* @return $this
*/
public function setChecksum($checksum)
{
$this->checksum = $checksum;
return $this;
}
/**
* @return string
*/
public function getChecksum()
{
return $this->checksum;
}
/**
* @param string $content
* @return $this
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* @param ContentBlockInterface $block
* @return $this
*/
public function addBlock(ContentBlockInterface $block)
{
$this->blocks[$block->getId()] = $block;
return $this;
}
/**
* @return string
*/
public function serialize()
{
return serialize($this->toArray());
}
/**
* @param string $serialized
*/
public function unserialize($serialized)
{
$array = unserialize($serialized);
$this->build($array);
}
/**
* @return string
*/
protected function generateId()
{
return uniqid('', true);
}
/**
* @param array $serialized
* @throws \RuntimeException
*/
protected function checkVersion(array $serialized)
{
$version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1;
if ($version !== $this->version) {
throw new \RuntimeException(sprintf('Unsupported version %s', $version));
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\ContentBlock;
/**
* ContentBlock Interface
* @package Grav\Framework\ContentBlock
*/
interface ContentBlockInterface extends \Serializable
{
/**
* @param string $id
* @return static
*/
public static function create($id = null);
/**
* @param array $serialized
* @return ContentBlockInterface
*/
public static function fromArray(array $serialized);
/**
* @param string $id
*/
public function __construct($id = null);
/**
* @return string
*/
public function getId();
/**
* @return string
*/
public function getToken();
/**
* @return array
*/
public function toArray();
/**
* @return string
*/
public function toString();
/**
* @return string
*/
public function __toString();
/**
* @param array $serialized
*/
public function build(array $serialized);
/**
* @param string $checksum
* @return $this
*/
public function setChecksum($checksum);
/**
* @return string
*/
public function getChecksum();
/**
* @param string $content
* @return $this
*/
public function setContent($content);
/**
* @param ContentBlockInterface $block
* @return $this
*/
public function addBlock(ContentBlockInterface $block);
}

View File

@@ -0,0 +1,386 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\ContentBlock;
/**
* HtmlBlock
*
* @package Grav\Framework\ContentBlock
*/
class HtmlBlock extends ContentBlock implements HtmlBlockInterface
{
protected $version = 1;
protected $frameworks = [];
protected $styles = [];
protected $scripts = [];
protected $html = [];
/**
* @return array
*/
public function getAssets()
{
$assets = $this->getAssetsFast();
$this->sortAssets($assets['styles']);
$this->sortAssets($assets['scripts']);
$this->sortAssets($assets['html']);
return $assets;
}
/**
* @return array
*/
public function getFrameworks()
{
$assets = $this->getAssetsFast();
return array_keys($assets['frameworks']);
}
/**
* @param string $location
* @return array
*/
public function getStyles($location = 'head')
{
return $this->getAssetsInLocation('styles', $location);
}
/**
* @param string $location
* @return array
*/
public function getScripts($location = 'head')
{
return $this->getAssetsInLocation('scripts', $location);
}
/**
* @param string $location
* @return array
*/
public function getHtml($location = 'bottom')
{
return $this->getAssetsInLocation('html', $location);
}
/**
* @return array[]
*/
public function toArray()
{
$array = parent::toArray();
if ($this->frameworks) {
$array['frameworks'] = $this->frameworks;
}
if ($this->styles) {
$array['styles'] = $this->styles;
}
if ($this->scripts) {
$array['scripts'] = $this->scripts;
}
if ($this->html) {
$array['html'] = $this->html;
}
return $array;
}
/**
* @param array $serialized
* @throws \RuntimeException
*/
public function build(array $serialized)
{
parent::build($serialized);
$this->frameworks = isset($serialized['frameworks']) ? (array) $serialized['frameworks'] : [];
$this->styles = isset($serialized['styles']) ? (array) $serialized['styles'] : [];
$this->scripts = isset($serialized['scripts']) ? (array) $serialized['scripts'] : [];
$this->html = isset($serialized['html']) ? (array) $serialized['html'] : [];
}
/**
* @param string $framework
* @return $this
*/
public function addFramework($framework)
{
$this->frameworks[$framework] = 1;
return $this;
}
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*
* @example $block->addStyle('assets/js/my.js');
* @example $block->addStyle(['href' => 'assets/js/my.js', 'media' => 'screen']);
*/
public function addStyle($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
$element = ['href' => (string) $element];
}
if (empty($element['href'])) {
return false;
}
if (!isset($this->styles[$location])) {
$this->styles[$location] = [];
}
$id = !empty($element['id']) ? ['id' => (string) $element['id']] : [];
$href = $element['href'];
$type = !empty($element['type']) ? (string) $element['type'] : 'text/css';
$media = !empty($element['media']) ? (string) $element['media'] : null;
unset(
$element['tag'],
$element['id'],
$element['rel'],
$element['content'],
$element['href'],
$element['type'],
$element['media']
);
$this->styles[$location][md5($href) . sha1($href)] = [
':type' => 'file',
':priority' => (int) $priority,
'href' => $href,
'type' => $type,
'media' => $media,
'element' => $element
] + $id;
return true;
}
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addInlineStyle($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
$element = ['content' => (string) $element];
}
if (empty($element['content'])) {
return false;
}
if (!isset($this->styles[$location])) {
$this->styles[$location] = [];
}
$content = (string) $element['content'];
$type = !empty($element['type']) ? (string) $element['type'] : 'text/css';
$this->styles[$location][md5($content) . sha1($content)] = [
':type' => 'inline',
':priority' => (int) $priority,
'content' => $content,
'type' => $type
];
return true;
}
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addScript($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
$element = ['src' => (string) $element];
}
if (empty($element['src'])) {
return false;
}
if (!isset($this->scripts[$location])) {
$this->scripts[$location] = [];
}
$src = $element['src'];
$type = !empty($element['type']) ? (string) $element['type'] : 'text/javascript';
$defer = isset($element['defer']) ? true : false;
$async = isset($element['async']) ? true : false;
$handle = !empty($element['handle']) ? (string) $element['handle'] : '';
$this->scripts[$location][md5($src) . sha1($src)] = [
':type' => 'file',
':priority' => (int) $priority,
'src' => $src,
'type' => $type,
'defer' => $defer,
'async' => $async,
'handle' => $handle
];
return true;
}
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addInlineScript($element, $priority = 0, $location = 'head')
{
if (!is_array($element)) {
$element = ['content' => (string) $element];
}
if (empty($element['content'])) {
return false;
}
if (!isset($this->scripts[$location])) {
$this->scripts[$location] = [];
}
$content = (string) $element['content'];
$type = !empty($element['type']) ? (string) $element['type'] : 'text/javascript';
$this->scripts[$location][md5($content) . sha1($content)] = [
':type' => 'inline',
':priority' => (int) $priority,
'content' => $content,
'type' => $type
];
return true;
}
/**
* @param string $html
* @param int $priority
* @param string $location
* @return bool
*/
public function addHtml($html, $priority = 0, $location = 'bottom')
{
if (empty($html) || !is_string($html)) {
return false;
}
if (!isset($this->html[$location])) {
$this->html[$location] = [];
}
$this->html[$location][md5($html) . sha1($html)] = [
':priority' => (int) $priority,
'html' => $html
];
return true;
}
/**
* @return array
*/
protected function getAssetsFast()
{
$assets = [
'frameworks' => $this->frameworks,
'styles' => $this->styles,
'scripts' => $this->scripts,
'html' => $this->html
];
foreach ($this->blocks as $block) {
if ($block instanceof HtmlBlock) {
$blockAssets = $block->getAssetsFast();
$assets['frameworks'] += $blockAssets['frameworks'];
foreach ($blockAssets['styles'] as $location => $styles) {
if (!isset($assets['styles'][$location])) {
$assets['styles'][$location] = $styles;
} elseif ($styles) {
$assets['styles'][$location] += $styles;
}
}
foreach ($blockAssets['scripts'] as $location => $scripts) {
if (!isset($assets['scripts'][$location])) {
$assets['scripts'][$location] = $scripts;
} elseif ($scripts) {
$assets['scripts'][$location] += $scripts;
}
}
foreach ($blockAssets['html'] as $location => $htmls) {
if (!isset($assets['html'][$location])) {
$assets['html'][$location] = $htmls;
} elseif ($htmls) {
$assets['html'][$location] += $htmls;
}
}
}
}
return $assets;
}
/**
* @param string $type
* @param string $location
* @return array
*/
protected function getAssetsInLocation($type, $location)
{
$assets = $this->getAssetsFast();
if (empty($assets[$type][$location])) {
return [];
}
$styles = $assets[$type][$location];
$this->sortAssetsInLocation($styles);
return $styles;
}
/**
* @param array $items
*/
protected function sortAssetsInLocation(array &$items)
{
$count = 0;
foreach ($items as &$item) {
$item[':order'] = ++$count;
}
unset($item);
uasort(
$items,
function ($a, $b) {
return ($a[':priority'] === $b[':priority'])
? $a[':order'] - $b[':order'] : $a[':priority'] - $b[':priority'];
}
);
}
/**
* @param array $array
*/
protected function sortAssets(array &$array)
{
foreach ($array as $location => &$items) {
$this->sortAssetsInLocation($items);
}
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\ContentBlock;
/**
* Interface HtmlBlockInterface
* @package Grav\Framework\ContentBlock
*/
interface HtmlBlockInterface extends ContentBlockInterface
{
/**
* @return array
*/
public function getAssets();
/**
* @return array
*/
public function getFrameworks();
/**
* @param string $location
* @return array
*/
public function getStyles($location = 'head');
/**
* @param string $location
* @return array
*/
public function getScripts($location = 'head');
/**
* @param string $location
* @return array
*/
public function getHtml($location = 'bottom');
/**
* @param string $framework
* @return $this
*/
public function addFramework($framework);
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*
* @example $block->addStyle('assets/js/my.js');
* @example $block->addStyle(['href' => 'assets/js/my.js', 'media' => 'screen']);
*/
public function addStyle($element, $priority = 0, $location = 'head');
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addInlineStyle($element, $priority = 0, $location = 'head');
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addScript($element, $priority = 0, $location = 'head');
/**
* @param string|array $element
* @param int $priority
* @param string $location
* @return bool
*/
public function addInlineScript($element, $priority = 0, $location = 'head');
/**
* @param string $html
* @param int $priority
* @param string $location
* @return bool
*/
public function addHtml($html, $priority = 0, $location = 'bottom');
}

View File

@@ -0,0 +1,44 @@
<?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();
/**
* 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);
}

View File

@@ -0,0 +1,85 @@
<?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;
class IniFormatter implements FormatterInterface
{
/** @var array */
private $config;
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $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();
}
/**
* {@inheritdoc}
*/
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)
{
$string = '';
foreach ($data as $key => $value) {
$string .= $key . '="' . preg_replace(
['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
['\"', '\\\\', '\t', '\n', '\r'],
$value
) . "\"\n";
}
return $string;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @parse_ini_string($data);
if ($decoded === false) {
throw new \RuntimeException('Decoding INI failed');
}
return $decoded;
}
}

View File

@@ -0,0 +1,80 @@
<?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;
class JsonFormatter implements FormatterInterface
{
/** @var array */
private $config;
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.json',
'encode_options' => 0,
'decode_assoc' => true
];
}
/**
* @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();
}
/**
* {@inheritdoc}
*/
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)
{
$encoded = @json_encode($data, $this->config['encode_options']);
if ($encoded === false) {
throw new \RuntimeException('Encoding JSON failed');
}
return $encoded;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @json_decode($data, $this->config['decode_assoc']);
if ($decoded === false) {
throw new \RuntimeException('Decoding JSON failed');
}
return $decoded;
}
}

View File

@@ -0,0 +1,118 @@
<?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;
class MarkdownFormatter implements FormatterInterface
{
/** @var array */
private $config;
/** @var FormatterInterface */
private $headerFormatter;
public function __construct(array $config = [], FormatterInterface $headerFormatter = null)
{
$this->config = $config + [
'file_extension' => '.md',
'header' => 'header',
'body' => 'markdown',
'raw' => 'frontmatter',
'yaml' => ['inline' => 20]
];
$this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']);
}
/**
* @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();
}
/**
* {@inheritdoc}
*/
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)
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$header = isset($data[$headerVar]) ? (array) $data[$headerVar] : [];
$body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : '';
// Create Markdown file with YAML header.
$encoded = '';
if ($header) {
$encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n";
}
$encoded .= $body;
// Normalize line endings to Unix style.
$encoded = preg_replace("/(\r\n|\r)/", "\n", $encoded);
return $encoded;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$rawVar = $this->config['raw'];
$content = [
$headerVar => [],
$bodyVar => ''
];
$headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis";
// Normalize line endings to Unix style.
$data = preg_replace("/(\r\n|\r)/", "\n", $data);
// Parse header.
preg_match($headerRegex, ltrim($data), $matches);
if(empty($matches)) {
$content[$bodyVar] = $data;
} else {
// Normalize frontmatter.
$frontmatter = preg_replace("/\n\t/", "\n ", $matches[1]);
if ($rawVar) {
$content[$rawVar] = $frontmatter;
}
$content[$headerVar] = $this->headerFormatter->decode($frontmatter);
$content[$bodyVar] = $matches[2];
}
return $content;
}
}

View File

@@ -0,0 +1,98 @@
<?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;
class SerializeFormatter implements FormatterInterface
{
/** @var array */
private $config;
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.ser'
];
}
/**
* @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();
}
/**
* {@inheritdoc}
*/
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)
{
return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r']));
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @unserialize($data);
if ($decoded === false) {
throw new \RuntimeException('Decoding serialized data failed');
}
return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]);
}
/**
* Preserve new lines, recursive function.
*
* @param mixed $data
* @param array $search
* @param array $replace
* @return mixed
*/
protected function preserveLines($data, $search, $replace)
{
if (is_string($data)) {
$data = str_replace($search, $replace, $data);
} elseif (is_array($data)) {
foreach ($data as &$value) {
$value = $this->preserveLines($value, $search, $replace);
}
unset($value);
}
return $data;
}
}

View File

@@ -0,0 +1,105 @@
<?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;
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
{
/** @var array */
private $config;
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.yaml',
'inline' => 5,
'indent' => 2,
'native' => true,
'compat' => true
];
}
/**
* @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();
}
/**
* {@inheritdoc}
*/
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)
{
try {
return (string) YamlParser::dump(
$data,
$inline ? (int) $inline : $this->config['inline'],
$indent ? (int) $indent : $this->config['indent'],
YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE
);
} catch (DumpException $e) {
throw new \RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
// Try native PECL YAML PHP extension first if available.
if ($this->config['native'] && function_exists('yaml_parse')) {
// Safely decode YAML.
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
$decoded = @yaml_parse($data);
@ini_set('yaml.decode_php', $saved);
if ($decoded !== false) {
return (array) $decoded;
}
}
try {
return (array) YamlParser::parse($data);
} catch (ParseException $e) {
if ($this->config['compat']) {
return (array) FallbackYamlParser::parse($data);
}
throw new \RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Access;
/**
* ArrayAccess Object Trait
* @package Grav\Framework\Object
*/
trait ArrayAccessTrait
{
/**
* Whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
return $this->hasProperty($offset);
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return $this->getProperty($offset);
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function offsetSet($offset, $value)
{
$this->setProperty($offset, $value);
}
/**
* Unsets an offset.
*
* @param mixed $offset The offset to unset.
*/
public function offsetUnset($offset)
{
$this->unsetProperty($offset);
}
abstract public function hasProperty($property);
abstract public function getProperty($property, $default = null);
abstract public function setProperty($property, $value);
abstract public function unsetProperty($property);
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Access;
/**
* Nested ArrayAccess Object Trait
* @package Grav\Framework\Object
*/
trait NestedArrayAccessTrait
{
/**
* Whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function offsetExists($offset)
{
return $this->hasNestedProperty($offset);
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function offsetGet($offset)
{
return $this->getNestedProperty($offset);
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function offsetSet($offset, $value)
{
$this->setNestedProperty($offset, $value);
}
/**
* Unsets an offset.
*
* @param mixed $offset The offset to unset.
*/
public function offsetUnset($offset)
{
$this->unsetNestedProperty($offset);
}
abstract public function hasNestedProperty($property, $separator = null);
abstract public function getNestedProperty($property, $default = null, $separator = null);
abstract public function setNestedProperty($property, $value, $separator = null);
abstract public function unsetNestedProperty($property, $separator = null);
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Access;
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
/**
* Nested Properties Collection Trait
* @package Grav\Framework\Object\Properties
*/
trait NestedPropertyCollectionTrait
{
/**
* @param string $property Object property to be matched.
* @param string $separator Separator, defaults to '.'
* @return array Key/Value pairs of the properties.
*/
public function hasNestedProperty($property, $separator = null)
{
$list = [];
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $id => $element) {
$list[$id] = $element->hasNestedProperty($property, $separator);
}
return $list;
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if not set.
* @param string $separator Separator, defaults to '.'
* @return array Key/Value pairs of the properties.
*/
public function getNestedProperty($property, $default = null, $separator = null)
{
$list = [];
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $id => $element) {
$list[$id] = $element->getNestedProperty($property, $default, $separator);
}
return $list;
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function setNestedProperty($property, $value, $separator = null)
{
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->setNestedProperty($property, $value, $separator);
}
return $this;
}
/**
* @param string $property Object property to be updated.
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function unsetNestedProperty($property, $separator = null)
{
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->unsetNestedProperty($property, $separator);
}
return $this;
}
/**
* @param string $property Object property to be updated.
* @param string $default Default value.
* @param string $separator Separator, defaults to '.'
* @return $this
*/
public function defNestedProperty($property, $default, $separator = null)
{
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->defNestedProperty($property, $default, $separator);
}
return $this;
}
/**
* Group items in the collection by a field.
*
* @param string $property Object property to be used to make groups.
* @param string $separator Separator, defaults to '.'
* @return array
*/
public function group($property, $separator = null)
{
$list = [];
/** @var NestedObjectInterface $element */
foreach ($this->getIterator() as $element) {
$list[(string) $element->getNestedProperty($property, null, $separator)][] = $element;
}
return $list;
}
/**
* @return \Traversable
*/
abstract public function getIterator();
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Access;
use Grav\Framework\Object\Interfaces\ObjectInterface;
/**
* Nested Property Object Trait
* @package Grav\Framework\Object\Traits
*/
trait NestedPropertyTrait
{
/**
* @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)
{
$test = new \stdClass;
return $this->getNestedProperty($property, $test, $separator) !== $test;
}
/**
* @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)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
if (!$this->hasProperty($offset)) {
return $default;
}
$current = $this->getProperty($offset);
while ($path) {
// Get property of nested Object.
if ($current instanceof ObjectInterface) {
if (method_exists($current, 'getNestedProperty')) {
return $current->getNestedProperty(implode($separator, $path), $default, $separator);
}
return $current->getProperty(implode($separator, $path), $default);
}
$offset = array_shift($path);
if ((is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) {
$current = $current[$offset];
} elseif (is_object($current) && isset($current->{$offset})) {
$current = $current->{$offset};
} else {
return $default;
}
};
return $current;
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @param string $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
public function setNestedProperty($property, $value, $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
if (!$path) {
$this->setProperty($offset, $value);
return $this;
}
$current = &$this->doGetProperty($offset, null, true);
while ($path) {
$offset = array_shift($path);
// Handle arrays and scalars.
if ($current === null) {
$current = [$offset => []];
} elseif (is_array($current)) {
if (!isset($current[$offset])) {
$current[$offset] = [];
}
} else {
throw new \RuntimeException('Cannot set nested property on non-array value');
}
$current = &$current[$offset];
};
$current = $value;
return $this;
}
/**
* @param string $property Object property to be updated.
* @param string $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
public function unsetNestedProperty($property, $separator = null)
{
$separator = $separator ?: '.';
$path = explode($separator, $property);
$offset = array_shift($path);
if (!$path) {
$this->unsetProperty($offset);
return $this;
}
$last = array_pop($path);
$current = &$this->doGetProperty($offset, null, true);
while ($path) {
$offset = array_shift($path);
// Handle arrays and scalars.
if ($current === null) {
return $this;
}
if (is_array($current)) {
if (!isset($current[$offset])) {
return $this;
}
} else {
throw new \RuntimeException('Cannot set nested property on non-array value');
}
$current = &$current[$offset];
};
unset($current[$last]);
return $this;
}
/**
* @param string $property Object property to be updated.
* @param string $default Default value.
* @param string $separator Separator, defaults to '.'
* @return $this
* @throws \RuntimeException
*/
public function defNestedProperty($property, $default, $separator = null)
{
if (!$this->hasNestedProperty($property, $separator)) {
$this->setNestedProperty($property, $default, $separator);
}
return $this;
}
abstract public function hasProperty($property);
abstract public function getProperty($property, $default = null);
abstract public function setProperty($property, $value);
abstract public function unsetProperty($property);
abstract protected function &doGetProperty($property, $default = null, $doCreate = false);
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Access;
/**
* Overloaded Property Object Trait
* @package Grav\Framework\Object\Access
*/
trait OverloadedPropertyTrait
{
/**
* Checks whether or not an offset exists.
*
* @param mixed $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function __isset($offset)
{
return $this->hasProperty($offset);
}
/**
* Returns the value at specified offset.
*
* @param mixed $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
public function __get($offset)
{
return $this->getProperty($offset);
}
/**
* Assigns a value to the specified offset.
*
* @param mixed $offset The offset to assign the value to.
* @param mixed $value The value to set.
*/
public function __set($offset, $value)
{
$this->setProperty($offset, $value);
}
/**
* Magic method to unset the attribute
*
* @param mixed $offset The name value to unset
*/
public function __unset($offset)
{
$this->unsetProperty($offset);
}
abstract public function hasProperty($property);
abstract public function getProperty($property, $default = null);
abstract public function setProperty($property, $value);
abstract public function unsetProperty($property);
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
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\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\ArrayPropertyTrait;
/**
* Array Object class.
*
* @package Grav\Framework\Object
*/
class ArrayObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, ArrayPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
}

View File

@@ -0,0 +1,180 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Base;
use Grav\Framework\Object\Interfaces\ObjectInterface;
/**
* ObjectCollection Trait
* @package Grav\Framework\Object
*/
trait ObjectCollectionTrait
{
use ObjectTrait {
setKey as public;
}
/**
* 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->call('getKey');
}
/**
* @param string $property Object property to be matched.
* @return array Key/Value pairs of the properties.
*/
public function doHasProperty($property)
{
$list = [];
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $id => $element) {
$list[$id] = $element->hasProperty($property);
}
return $list;
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if not set.
* @return array Key/Value pairs of the properties.
*/
public function doGetProperty($property, $default = null)
{
$list = [];
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $id => $element) {
$list[$id] = $element->getProperty($property, $default);
}
return $list;
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @return $this
*/
public function doSetProperty($property, $value)
{
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->setProperty($property, $value);
}
return $this;
}
/**
* @param string $property Object property to be updated.
* @return $this
*/
public function doUnsetProperty($property)
{
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->unsetProperty($property);
}
return $this;
}
/**
* @param string $property Object property to be updated.
* @param string $default Default value.
* @return $this
*/
public function doDefProperty($property, $default)
{
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $element) {
$element->defProperty($property, $default);
}
return $this;
}
/**
* @param string $method Method name.
* @param array $arguments List of arguments passed to the function.
* @return array Return values.
*/
public function call($method, array $arguments = [])
{
$list = [];
foreach ($this->getIterator() as $id => $element) {
$list[$id] = method_exists($element, $method)
? call_user_func_array([$element, $method], $arguments) : null;
}
return $list;
}
/**
* Group items in the collection by a field and return them as associated array.
*
* @param string $property
* @return array
*/
public function group($property)
{
$list = [];
/** @var ObjectInterface $element */
foreach ($this->getIterator() as $element) {
$list[(string) $element->getProperty($property)][] = $element;
}
return $list;
}
/**
* Group items in the collection by a field and return them as associated array of collections.
*
* @param string $property
* @return static[]
*/
public function collectionGroup($property)
{
$collections = [];
foreach ($this->group($property) as $id => $elements) {
$collection = $this->createFrom($elements);
$collections[$id] = $collection;
}
return $collections;
}
/**
* @return \Traversable
*/
abstract public function getIterator();
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Base;
/**
* Object trait.
*
* @package Grav\Framework\Object
*/
trait ObjectTrait
{
/** @var string */
static protected $type;
/**
* @var string
*/
private $_key;
/**
* @return string
*/
protected function getTypePrefix()
{
return '';
}
/**
* @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 $property Object property name.
* @return bool True if property has been defined (can be null).
*/
public function hasProperty($property)
{
return $this->doHasProperty($property);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return mixed Property value.
*/
public function getProperty($property, $default = null)
{
return $this->doGetProperty($property, $default);
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @return $this
*/
public function setProperty($property, $value)
{
$this->doSetProperty($property, $value);
return $this;
}
/**
* @param string $property Object property to be unset.
* @return $this
*/
public function unsetProperty($property)
{
$this->doUnsetProperty($property);
return $this;
}
/**
* @param string $property Object property to be defined.
* @param mixed $default Default value.
* @return $this
*/
public function defProperty($property, $default)
{
if (!$this->hasProperty($property)) {
$this->setProperty($property, $default);
}
return $this;
}
/**
* Implements Serializable interface.
*
* @return string
*/
public function serialize()
{
return serialize($this->doSerialize());
}
/**
* @param string $serialized
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
if (method_exists($this, 'initObjectProperties')) {
$this->initObjectProperties();
}
$this->doUnserialize($data);
}
/**
* @return array
*/
protected function doSerialize()
{
return $this->jsonSerialize();
}
/**
* @param array $serialized
*/
protected function doUnserialize(array $serialized)
{
if (!isset($serialized['key'], $serialized['type'], $serialized['elements']) || $serialized['type'] !== $this->getType()) {
throw new \InvalidArgumentException("Cannot unserialize '{$this->getType()}': Bad data");
}
$this->setKey($serialized['key']);
$this->setElements($serialized['elements']);
}
/**
* Implements JsonSerializable interface.
*
* @return array
*/
public function jsonSerialize()
{
return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()];
}
/**
* Returns a string representation of this object.
*
* @return string
*/
public function __toString()
{
return $this->getKey();
}
/**
* @param string $key
* @return $this
*/
protected function setKey($key)
{
$this->_key = (string) $key;
return $this;
}
abstract protected function doHasProperty($property);
abstract protected function &doGetProperty($property, $default = null, $doCreate = false);
abstract protected function doSetProperty($property, $value);
abstract protected function doUnsetProperty($property);
abstract protected function getElements();
abstract protected function setElements(array $elements);
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Collection;
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use Doctrine\Common\Collections\Expr\Comparison;
class ObjectExpressionVisitor extends ClosureExpressionVisitor
{
/**
* Accesses the field of a given object.
*
* @param object $object
* @param string $field
*
* @return mixed
*/
public static function getObjectFieldValue($object, $field)
{
$op = $value = null;
$pos = strpos($field, '(');
if (false !== $pos) {
list ($op, $field) = explode('(', $field, 2);
$field = rtrim($field, ')');
}
if (isset($object[$field])) {
$value = $object[$field];
} else {
$accessors = array('', 'get', 'is');
foreach ($accessors as $accessor) {
$accessor .= $field;
if (!method_exists($object, $accessor)) {
continue;
}
$value = $object->{$accessor}();
break;
}
}
if ($op) {
$function = 'filter' . ucfirst(strtolower($op));
if (method_exists(static::class, $function)) {
$value = static::$function($value);
}
}
return $value;
}
public static function filterLower($str)
{
return mb_strtolower($str);
}
public static function filterUpper($str)
{
return mb_strtoupper($str);
}
public static function filterLength($str)
{
return mb_strlen($str);
}
public static function filterLtrim($str)
{
return ltrim($str);
}
public static function filterRtrim($str)
{
return rtrim($str);
}
public static function filterTrim($str)
{
return trim($str);
}
/**
* Helper for sorting arrays of objects based on multiple fields + orientations.
*
* @param string $name
* @param int $orientation
* @param \Closure $next
*
* @return \Closure
*/
public static function sortByField($name, $orientation = 1, \Closure $next = null)
{
if (!$next) {
$next = function() {
return 0;
};
}
return function ($a, $b) use ($name, $next, $orientation) {
$aValue = static::getObjectFieldValue($a, $name);
$bValue = static::getObjectFieldValue($b, $name);
if ($aValue === $bValue) {
return $next($a, $b);
}
return (($aValue > $bValue) ? 1 : -1) * $orientation;
};
}
/**
* {@inheritDoc}
*/
public function walkComparison(Comparison $comparison)
{
$field = $comparison->getField();
$value = $comparison->getValue()->getValue(); // shortcut for walkValue()
switch ($comparison->getOperator()) {
case Comparison::EQ:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) === $value;
};
case Comparison::NEQ:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) !== $value;
};
case Comparison::LT:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) < $value;
};
case Comparison::LTE:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) <= $value;
};
case Comparison::GT:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) > $value;
};
case Comparison::GTE:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) >= $value;
};
case Comparison::IN:
return function ($object) use ($field, $value) {
return \in_array(static::getObjectFieldValue($object, $field), $value, true);
};
case Comparison::NIN:
return function ($object) use ($field, $value) {
return !\in_array(static::getObjectFieldValue($object, $field), $value, true);
};
case Comparison::CONTAINS:
return function ($object) use ($field, $value) {
return false !== strpos(static::getObjectFieldValue($object, $field), $value);
};
case Comparison::MEMBER_OF:
return function ($object) use ($field, $value) {
$fieldValues = static::getObjectFieldValue($object, $field);
if (!is_array($fieldValues)) {
$fieldValues = iterator_to_array($fieldValues);
}
return \in_array($value, $fieldValues, true);
};
case Comparison::STARTS_WITH:
return function ($object) use ($field, $value) {
return 0 === strpos(static::getObjectFieldValue($object, $field), $value);
};
case Comparison::ENDS_WITH:
return function ($object) use ($field, $value) {
return $value === substr(static::getObjectFieldValue($object, $field), -strlen($value));
};
default:
throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator());
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Interfaces;
/**
* Object Interface
* @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).
*/
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.
*/
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 '.'
* @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 '.'
* @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 '.'
* @return $this
* @throws \RuntimeException
*/
public function unsetNestedProperty($property, $separator = null);
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Interfaces;
use Doctrine\Common\Collections\Selectable;
use Grav\Framework\Collection\CollectionInterface;
/**
* ObjectCollection Interface
* @package Grav\Framework\Collection
*/
interface ObjectCollectionInterface extends CollectionInterface, Selectable, ObjectInterface
{
/**
* Create a copy from this collection by cloning all objects in the collection.
*
* @return static
*/
public function copy();
/**
* @param string $key
* @return $this
*/
public function setKey($key);
/**
* @return array
*/
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.
* @return array Return values.
*/
public function call($name, array $arguments);
/**
* Group items in the collection by a field and return them as associated array.
*
* @param string $property
* @return array
*/
public function group($property);
/**
* Group items in the collection by a field and return them as associated array of collections.
*
* @param string $property
* @return static[]
*/
public function collectionGroup($property);
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Interfaces;
/**
* Object Interface
* @package Grav\Framework\Object
*/
interface ObjectInterface extends \Serializable, \JsonSerializable
{
/**
* @return string
*/
public function getType();
/**
* @return string
*/
public function getKey();
/**
* @param string $property Object property name.
* @return 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.
*/
public function getProperty($property, $default = null);
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @return $this
*/
public function setProperty($property, $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.
* @return $this
*/
public function unsetProperty($property);
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
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\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\LazyPropertyTrait;
/**
* Lazy Object class.
*
* @package Grav\Framework\Object
*/
class LazyObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, LazyPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 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\ArrayCollection;
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;
use Grav\Framework\Object\Base\ObjectCollectionTrait;
use Grav\Framework\Object\Collection\ObjectExpressionVisitor;
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
/**
* Object Collection
* @package Grav\Framework\Object
*/
class ObjectCollection extends ArrayCollection implements ObjectCollectionInterface, NestedObjectInterface
{
use ObjectCollectionTrait, NestedPropertyCollectionTrait {
NestedPropertyCollectionTrait::group insteadof ObjectCollectionTrait;
}
/**
* @param array $elements
* @param string $key
* @throws \InvalidArgumentException
*/
public function __construct(array $elements = [], $key = null)
{
parent::__construct($this->setElements($elements));
$this->setKey($key);
}
/**
* {@inheritDoc}
*/
public function matching(Criteria $criteria)
{
$expr = $criteria->getWhereExpression();
$filtered = $this->getElements();
if ($expr) {
$visitor = new ObjectExpressionVisitor();
$filter = $visitor->dispatch($expr);
$filtered = array_filter($filtered, $filter);
}
if ($orderings = $criteria->getOrderings()) {
$next = null;
foreach (array_reverse($orderings) as $field => $ordering) {
$next = ObjectExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
}
uasort($filtered, $next);
}
$offset = $criteria->getFirstResult();
$length = $criteria->getMaxResults();
if ($offset || $length) {
$filtered = array_slice($filtered, (int)$offset, $length);
}
return $this->createFrom($filtered);
}
protected function getElements()
{
return $this->toArray();
}
protected function setElements(array $elements)
{
return $elements;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Property;
/**
* Array Property Trait
*
* Stores all object properties into an array.
*
* @package Grav\Framework\Object\Property
*/
trait ArrayPropertyTrait
{
/**
* Properties of the object.
* @var array
*/
private $_elements;
/**
* @param array $elements
* @param string $key
* @throws \InvalidArgumentException
*/
public function __construct(array $elements = [], $key = null)
{
$this->setElements($elements);
$this->setKey($key);
}
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
*/
protected function doHasProperty($property)
{
return array_key_exists($property, $this->_elements);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @param bool $doCreate Set true to create variable.
* @return mixed Property value.
*/
protected function &doGetProperty($property, $default = null, $doCreate = false)
{
if (!array_key_exists($property, $this->_elements)) {
if ($doCreate) {
$this->_elements[$property] = null;
} else {
return $default;
}
}
return $this->_elements[$property];
}
/**
* @param string $property Object property to be updated.
* @param mixed $value New value.
*/
protected function doSetProperty($property, $value)
{
$this->_elements[$property] = $value;
}
/**
* @param string $property Object property to be unset.
*/
protected function doUnsetProperty($property)
{
unset($this->_elements[$property]);
}
/**
* @param string $property
* @param mixed|null $default
* @return mixed|null
*/
protected function getElement($property, $default = null)
{
return array_key_exists($property, $this->_elements) ? $this->_elements[$property] : $default;
}
/**
* @return array
*/
protected function getElements()
{
return $this->_elements;
}
/**
* @param array $elements
*/
protected function setElements(array $elements)
{
$this->_elements = $elements;
}
abstract protected function setKey($key);
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Property;
/**
* Lazy Mixed Property Trait
*
* Stores defined object properties as class member variables and the rest into an array. Object properties are lazy
* loaded from the array.
*
* You may define following methods for the member variables:
* - `$this->offsetLoad($offset, $value)` called first time object property gets accessed
* - `$this->offsetPrepare($offset, $value)` called on every object property set
* - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed
*
* @package Grav\Framework\Object\Property
*/
trait LazyPropertyTrait
{
use ArrayPropertyTrait, ObjectPropertyTrait {
ObjectPropertyTrait::__construct insteadof ArrayPropertyTrait;
ArrayPropertyTrait::doHasProperty as hasArrayProperty;
ArrayPropertyTrait::doGetProperty as getArrayProperty;
ArrayPropertyTrait::doSetProperty as setArrayProperty;
ArrayPropertyTrait::doUnsetProperty as unsetArrayProperty;
ArrayPropertyTrait::getElement as getArrayElement;
ArrayPropertyTrait::getElements as getArrayElements;
ArrayPropertyTrait::setElements insteadof ObjectPropertyTrait;
ObjectPropertyTrait::doHasProperty as hasObjectProperty;
ObjectPropertyTrait::doGetProperty as getObjectProperty;
ObjectPropertyTrait::doSetProperty as setObjectProperty;
ObjectPropertyTrait::doUnsetProperty as unsetObjectProperty;
ObjectPropertyTrait::getElement as getObjectElement;
ObjectPropertyTrait::getElements as getObjectElements;
}
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
*/
protected function doHasProperty($property)
{
return $this->hasArrayProperty($property) || $this->hasObjectProperty($property);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return mixed Property value.
*/
protected function &doGetProperty($property, $default = null, $doCreate = false)
{
if ($this->hasObjectProperty($property)) {
return $this->getObjectProperty($property, $default, function ($default = null) use ($property) {
return $this->getArrayProperty($property, $default);
});
}
return $this->getArrayProperty($property, $default, $doCreate);
}
/**
* @param string $property Object property to be updated.
* @param mixed $value New value.
* @return $this
*/
protected function doSetProperty($property, $value)
{
if ($this->hasObjectProperty($property)) {
$this->setObjectProperty($property, $value);
} else {
$this->setArrayProperty($property, $value);
}
return $this;
}
/**
* @param string $property Object property to be unset.
* @return $this
*/
protected function doUnsetProperty($property)
{
$this->hasObjectProperty($property) ?
$this->unsetObjectProperty($property) : $this->unsetArrayProperty($property);
return $this;
}
/**
* @param string $property
* @param mixed|null $default
* @return mixed|null
*/
protected function getElement($property, $default = null)
{
if ($this->isPropertyLoaded($property)) {
return $this->getObjectElement($property, $default);
}
return $this->getArrayElement($property, $default);
}
/**
* @return array
*/
protected function getElements()
{
return $this->getObjectElements() + $this->getArrayElements();
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Property;
/**
* Mixed Property Trait
*
* Stores defined object properties as class member variables and the rest into an array.
*
* You may define following methods for member variables:
* - `$this->offsetLoad($offset, $value)` called first time object property gets accessed
* - `$this->offsetPrepare($offset, $value)` called on every object property set
* - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed
*
* @package Grav\Framework\Object\Property
*/
trait MixedPropertyTrait
{
use ArrayPropertyTrait, ObjectPropertyTrait {
ObjectPropertyTrait::__construct insteadof ArrayPropertyTrait;
ArrayPropertyTrait::doHasProperty as hasArrayProperty;
ArrayPropertyTrait::doGetProperty as getArrayProperty;
ArrayPropertyTrait::doSetProperty as setArrayProperty;
ArrayPropertyTrait::doUnsetProperty as unsetArrayProperty;
ArrayPropertyTrait::getElement as getArrayElement;
ArrayPropertyTrait::getElements as getArrayElements;
ArrayPropertyTrait::setElements as setArrayElements;
ObjectPropertyTrait::doHasProperty as hasObjectProperty;
ObjectPropertyTrait::doGetProperty as getObjectProperty;
ObjectPropertyTrait::doSetProperty as setObjectProperty;
ObjectPropertyTrait::doUnsetProperty as unsetObjectProperty;
ObjectPropertyTrait::getElement as getObjectElement;
ObjectPropertyTrait::getElements as getObjectElements;
ObjectPropertyTrait::setElements as setObjectElements;
}
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
*/
protected function doHasProperty($property)
{
return $this->hasArrayProperty($property) || $this->hasObjectProperty($property);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @return mixed Property value.
*/
protected function &doGetProperty($property, $default = null, $doCreate = false)
{
if ($this->hasObjectProperty($property)) {
return $this->getObjectProperty($property);
}
return $this->getArrayProperty($property, $default, $doCreate);
}
/**
* @param string $property Object property to be updated.
* @param mixed $value New value.
* @return $this
*/
protected function doSetProperty($property, $value)
{
$this->hasObjectProperty($property)
? $this->setObjectProperty($property, $value) : $this->setArrayProperty($property, $value);
return $this;
}
/**
* @param string $property Object property to be unset.
* @return $this
*/
protected function doUnsetProperty($property)
{
$this->hasObjectProperty($property) ?
$this->unsetObjectProperty($property) : $this->unsetArrayProperty($property);
return $this;
}
/**
* @param string $property
* @param mixed|null $default
* @return mixed|null
*/
protected function getElement($property, $default = null)
{
if ($this->hasObjectProperty($property)) {
return $this->getObjectElement($property, $default);
}
return $this->getArrayElement($property, $default);
}
/**
* @return array
*/
protected function getElements()
{
return $this->getObjectElements() + $this->getArrayElements();
}
/**
* @param array $elements
*/
protected function setElements(array $elements)
{
$this->setObjectElements(array_intersect_key($elements, $this->_definedProperties));
$this->setArrayElements(array_diff_key($elements, $this->_definedProperties));
}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Property;
/**
* Object Property Trait
*
* Stores all properties as class member variables or object properties. All properties need to be defined as protected
* properties. Undefined properties will throw an error.
*
* Additionally you may define following methods:
* - `$this->offsetLoad($offset, $value)` called first time object property gets accessed
* - `$this->offsetPrepare($offset, $value)` called on every object property set
* - `$this->offsetSerialize($offset, $value)` called when the raw or serialized object property value is needed
*
* @package Grav\Framework\Object\Property
*/
trait ObjectPropertyTrait
{
/**
* @var array
*/
private $_definedProperties;
/**
* @param array $elements
* @param string $key
* @throws \InvalidArgumentException
*/
public function __construct(array $elements = [], $key = null)
{
$this->initObjectProperties();
$this->setElements($elements);
$this->setKey($key);
}
/**
* @param string $property Object property name.
* @return bool True if property has been loaded.
*/
protected function isPropertyLoaded($property)
{
return !empty($this->_definedProperties[$property]);
}
/**
* @param string $offset
* @param mixed $value
* @return mixed
*/
protected function offsetLoad($offset, $value)
{
$methodName = "offsetLoad_{$offset}";
return method_exists($this, $methodName)? $this->{$methodName}($value) : $value;
}
/**
* @param string $offset
* @param mixed $value
* @return mixed
*/
protected function offsetPrepare($offset, $value)
{
$methodName = "offsetPrepare_{$offset}";
return method_exists($this, $methodName) ? $this->{$methodName}($value) : $value;
}
/**
* @param string $offset
* @param mixed $value
* @return mixed
*/
protected function offsetSerialize($offset, $value)
{
$methodName = "offsetSerialize_{$offset}";
return method_exists($this, $methodName) ? $this->{$methodName}($value) : $value;
}
/**
* @param string $property Object property name.
* @return bool True if property has been defined (can be null).
*/
protected function doHasProperty($property)
{
return array_key_exists($property, $this->_definedProperties);
}
/**
* @param string $property Object property to be fetched.
* @param mixed $default Default value if property has not been set.
* @param callable|bool $doCreate Set true to create variable.
* @return mixed Property value.
*/
protected function &doGetProperty($property, $default = null, $doCreate = false)
{
if (!array_key_exists($property, $this->_definedProperties)) {
throw new \InvalidArgumentException("Property '{$property}' does not exist in the object!");
}
if (empty($this->_definedProperties[$property])) {
if ($doCreate === true) {
$this->_definedProperties[$property] = true;
$this->{$property} = null;
} elseif (is_callable($doCreate)) {
$this->_definedProperties[$property] = true;
$this->{$property} = $this->offsetLoad($property, $doCreate());
} else {
return $default;
}
}
return $this->{$property};
}
/**
* @param string $property Object property to be updated.
* @param mixed $value New value.
* @throws \InvalidArgumentException
*/
protected function doSetProperty($property, $value)
{
if (!array_key_exists($property, $this->_definedProperties)) {
throw new \InvalidArgumentException("Property '{$property}' does not exist in the object!");
}
$this->_definedProperties[$property] = true;
$this->{$property} = $this->offsetPrepare($property, $value);
}
/**
* @param string $property Object property to be unset.
*/
protected function doUnsetProperty($property)
{
if (!array_key_exists($property, $this->_definedProperties)) {
return;
}
$this->_definedProperties[$property] = false;
unset($this->{$property});
}
protected function initObjectProperties()
{
$this->_definedProperties = [];
foreach (get_object_vars($this) as $property => $value) {
if ($property[0] !== '_') {
$this->_definedProperties[$property] = ($value !== null);
}
}
}
/**
* @param string $property
* @param mixed|null $default
* @return mixed|null
*/
protected function getElement($property, $default = null)
{
if (empty($this->_definedProperties[$property])) {
return $default;
}
return $this->offsetSerialize($property, $this->{$property});
}
/**
* @return array
*/
protected function getElements()
{
$properties = array_intersect_key(get_object_vars($this), array_filter($this->_definedProperties));
$elements = [];
foreach ($properties as $offset => $value) {
$elements[$offset] = $this->offsetSerialize($offset, $value);
}
return $elements;
}
/**
* @param array $elements
*/
protected function setElements(array $elements)
{
foreach ($elements as $property => $value) {
$this->setProperty($property, $value);
}
}
abstract public function setProperty($property, $value);
abstract protected function setKey($key);
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
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\Object\Interfaces\NestedObjectInterface;
use Grav\Framework\Object\Property\ObjectPropertyTrait;
/**
* Property Object class.
*
* @package Grav\Framework\Object
*/
class PropertyObject implements NestedObjectInterface, \ArrayAccess
{
use ObjectTrait, ObjectPropertyTrait, NestedPropertyTrait, OverloadedPropertyTrait, NestedArrayAccessTrait;
}

View File

@@ -0,0 +1,406 @@
<?php
/**
* @package Grav\Framework\Psr7
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Psr7;
use Grav\Framework\Uri\UriPartsFilter;
use Psr\Http\Message\UriInterface;
/**
* Bare minimum PSR7 implementation.
*
* @package Grav\Framework\Uri\Psr7
*/
abstract class AbstractUri implements UriInterface
{
protected static $defaultPorts = [
'http' => 80,
'https' => 443
];
/** @var string Uri scheme. */
private $scheme = '';
/** @var string Uri user. */
private $user = '';
/** @var string Uri password. */
private $password = '';
/** @var string Uri host. */
private $host = '';
/** @var int|null Uri port. */
private $port;
/** @var string Uri path. */
private $path = '';
/** @var string Uri query string (without ?). */
private $query = '';
/** @var string Uri fragment (without #). */
private $fragment = '';
/**
* Please define constructor which calls $this->init().
*/
abstract public function __construct();
/**
* @inheritdoc
*/
public function getScheme()
{
return $this->scheme;
}
/**
* @inheritdoc
*/
public function getAuthority()
{
$authority = $this->host;
$userInfo = $this->getUserInfo();
if ($userInfo !== '') {
$authority = $userInfo . '@' . $authority;
}
if ($this->port !== null) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* @inheritdoc
*/
public function getUserInfo()
{
$userInfo = $this->user;
if ($this->password !== '') {
$userInfo .= ':' . $this->password;
}
return $userInfo;
}
/**
* @inheritdoc
*/
public function getHost()
{
return $this->host;
}
/**
* @inheritdoc
*/
public function getPort()
{
return $this->port;
}
/**
* @inheritdoc
*/
public function getPath()
{
return $this->path;
}
/**
* @inheritdoc
*/
public function getQuery()
{
return $this->query;
}
/**
* @inheritdoc
*/
public function getFragment()
{
return $this->fragment;
}
/**
* @inheritdoc
*/
public function withScheme($scheme)
{
$scheme = UriPartsFilter::filterScheme($scheme);
if ($this->scheme === $scheme) {
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
$new->unsetDefaultPort();
$new->validate();
return $new;
}
/**
* @inheritdoc
* @throws \InvalidArgumentException
*/
public function withUserInfo($user, $password = '')
{
$user = UriPartsFilter::filterUserInfo($user);
$password = UriPartsFilter::filterUserInfo($password);
if ($this->user === $user && $this->password === $password) {
return $this;
}
$new = clone $this;
$new->user = $user;
$new->password = $user !== '' ? $password : '';
$new->validate();
return $new;
}
/**
* @inheritdoc
*/
public function withHost($host)
{
$host = UriPartsFilter::filterHost($host);
if ($this->host === $host) {
return $this;
}
$new = clone $this;
$new->host = $host;
$new->validate();
return $new;
}
/**
* @inheritdoc
*/
public function withPort($port)
{
$port = UriPartsFilter::filterPort($port);
if ($this->port === $port) {
return $this;
}
$new = clone $this;
$new->port = $port;
$new->unsetDefaultPort();
$new->validate();
return $new;
}
/**
* @inheritdoc
*/
public function withPath($path)
{
$path = UriPartsFilter::filterPath($path);
if ($this->path === $path) {
return $this;
}
$new = clone $this;
$new->path = $path;
$new->validate();
return $new;
}
/**
* @inheritdoc
*/
public function withQuery($query)
{
$query = UriPartsFilter::filterQueryOrFragment($query);
if ($this->query === $query) {
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* @inheritdoc
* @throws \InvalidArgumentException
*/
public function withFragment($fragment)
{
$fragment = UriPartsFilter::filterQueryOrFragment($fragment);
if ($this->fragment === $fragment) {
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* @return string
*/
public function __toString()
{
return $this->getUrl();
}
/**
* @return array
*/
protected function getParts()
{
return [
'scheme' => $this->scheme,
'host' => $this->host,
'port' => $this->port,
'user' => $this->user,
'pass' => $this->password,
'path' => $this->path,
'query' => $this->query,
'fragment' => $this->fragment
];
}
/**
* Return the fully qualified base URL ( like http://getgrav.org ).
*
* Note that this method never includes a trailing /
*
* @return string
*/
protected function getBaseUrl()
{
$uri = '';
$scheme = $this->getScheme();
if ($scheme !== '') {
$uri .= $scheme . ':';
}
$authority = $this->getAuthority();
if ($authority !== '' || $scheme === 'file') {
$uri .= '//' . $authority;
}
return $uri;
}
/**
* @return string
*/
protected function getUrl()
{
$uri = $this->getBaseUrl() . $this->getPath();
$query = $this->getQuery();
if ($query !== '') {
$uri .= '?' . $query;
}
$fragment = $this->getFragment();
if ($fragment !== '') {
$uri .= '#' . $fragment;
}
return $uri;
}
/**
* @return string
*/
protected function getUser()
{
return $this->user;
}
/**
* @return string
*/
protected function getPassword()
{
return $this->password;
}
/**
* @param array $parts
* @throws \InvalidArgumentException
*/
protected function initParts(array $parts)
{
$this->scheme = isset($parts['scheme']) ? UriPartsFilter::filterScheme($parts['scheme']) : '';
$this->user = isset($parts['user']) ? UriPartsFilter::filterUserInfo($parts['user']) : '';
$this->password = isset($parts['pass']) ? UriPartsFilter::filterUserInfo($parts['pass']) : '';
$this->host = isset($parts['host']) ? UriPartsFilter::filterHost($parts['host']) : '';
$this->port = isset($parts['port']) ? UriPartsFilter::filterPort((int)$parts['port']) : null;
$this->path = isset($parts['path']) ? UriPartsFilter::filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? UriPartsFilter::filterQueryOrFragment($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? UriPartsFilter::filterQueryOrFragment($parts['fragment']) : '';
$this->unsetDefaultPort();
$this->validate();
}
/**
* @throws \InvalidArgumentException
*/
private function validate()
{
if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
throw new \InvalidArgumentException('Uri with a scheme must have a host');
}
if ($this->getAuthority() === '') {
if (0 === strpos($this->path, '//')) {
throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes \'//\'');
}
if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
}
} elseif (isset($this->path[0]) && $this->path[0] !== '/') {
throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash \'/\' or be empty');
}
}
protected function isDefaultPort()
{
$scheme = $this->scheme;
$port = $this->port;
return $this->port === null
|| (isset(static::$defaultPorts[$scheme]) && $port === static::$defaultPorts[$scheme]);
}
private function unsetDefaultPort()
{
if ($this->isDefaultPort()) {
$this->port = null;
}
}
}

View File

@@ -0,0 +1,300 @@
<?php
/**
* @package Grav\Framework\Route
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Route;
use Grav\Framework\Uri\UriFactory;
/**
* Implements Grav Route.
*
* @package Grav\Framework\Route
*/
class Route
{
/** @var string */
private $root = '';
/** @var string */
private $language = '';
/** @var string */
private $route = '';
/** @var array */
private $gravParams = [];
/** @var array */
private $queryParams = [];
/**
* You can use `RouteFactory` functions to create new `Route` objects.
*
* @param array $parts
* @throws \InvalidArgumentException
*/
public function __construct(array $parts = [])
{
$this->initParts($parts);
}
/**
* @return array
*/
public function getParts()
{
return [
'path' => $this->getUriPath(),
'query' => $this->getUriQuery(),
'grav' => [
'root' => $this->root,
'language' => $this->language,
'route' => $this->route,
'grav_params' => $this->gravParams,
'query_params' => $this->queryParams,
],
];
}
/**
* @return string
*/
public function getRootPrefix()
{
return $this->root;
}
/**
* @return string
*/
public function getLanguagePrefix()
{
return $this->language !== '' ? '/' . $this->language : '';
}
/**
* @param int $offset
* @param int|null $length
* @return string
*/
public function getRoute($offset = 0, $length = null)
{
if ($offset !== 0 || $length !== null) {
return ($offset === 0 ? '/' : '') . implode('/', $this->getRouteParts($offset, $length));
}
return '/' . $this->route;
}
/**
* @param int $offset
* @param int|null $length
* @return array
*/
public function getRouteParts($offset = 0, $length = null)
{
$parts = explode('/', $this->route);
if ($offset !== 0 || $length !== null) {
$parts = array_slice($parts, $offset, $length);
}
return $parts;
}
/**
* Return array of both query and Grav parameters.
*
* If a parameter exists in both, prefer Grav parameter.
*
* @return array
*/
public function getParams()
{
return $this->gravParams + $this->queryParams;
}
/**
* @return array
*/
public function getGravParams()
{
return $this->gravParams;
}
/**
* @return array
*/
public function getQueryParams()
{
return $this->queryParams;
}
/**
* Return value of the parameter, looking into both Grav parameters and query parameters.
*
* If the parameter exists in both, return Grav parameter.
*
* @param string $param
* @return string|null
*/
public function getParam($param)
{
$value = $this->getGravParam($param);
if ($value === null) {
$value = $this->getQueryParam($param);
}
return $value;
}
/**
* @param string $param
* @return string|null
*/
public function getGravParam($param)
{
return isset($this->gravParams[$param]) ? $this->gravParams[$param] : null;
}
/**
* @param string $param
* @return string|null
*/
public function getQueryParam($param)
{
return isset($this->queryParams[$param]) ? $this->queryParams[$param] : null;
}
/**
* @param string $param
* @param mixed $value
* @return Route
*/
public function withGravParam($param, $value)
{
return $this->withParam('gravParams', $param, null !== $value ? (string)$value : null);
}
/**
* @param string $param
* @param mixed $value
* @return Route
*/
public function withQueryParam($param, $value)
{
return $this->withParam('queryParams', $param, $value);
}
/**
* @return \Grav\Framework\Uri\Uri
*/
public function getUri()
{
return UriFactory::createFromParts($this->getParts());
}
/**
* @return string
*/
public function __toString()
{
$url = $this->getUriPath();
if ($this->queryParams) {
$url .= '?' . $this->getUriQuery();
}
return $url;
}
/**
* @param string $type
* @param string $param
* @param mixed $value
* @return static
*/
protected function withParam($type, $param, $value)
{
$oldValue = isset($this->{$type}[$param]) ? $this->{$type}[$param] : null;
if ($oldValue === $value) {
return $this;
}
$new = clone $this;
if ($value === null) {
unset($new->{$type}[$param]);
} else {
$new->{$type}[$param] = $value;
}
return $new;
}
/**
* @return string
*/
protected function getUriPath()
{
$parts = [$this->root];
if ($this->language !== '') {
$parts[] = $this->language;
}
if ($this->route !== '') {
$parts[] = $this->route;
}
if ($this->gravParams) {
$parts[] = RouteFactory::buildParams($this->gravParams);
}
return implode('/', $parts);
}
/**
* @return string
*/
protected function getUriQuery()
{
return UriFactory::buildQuery($this->queryParams);
}
/**
* @param array $parts
*/
protected function initParts(array $parts)
{
if (isset($parts['grav'])) {
$gravParts = $parts['grav'];
$this->root = $gravParts['root'];
$this->language = $gravParts['language'];
$this->route = $gravParts['route'];
$this->gravParams = $gravParts['params'];
$this->queryParams = $parts['query_params'];
} else {
$this->root = RouteFactory::getRoot();
$this->language = RouteFactory::getLanguage();
$path = isset($parts['path']) ? $parts['path'] : '/';
if (isset($parts['params'])) {
$this->route = trim(rawurldecode($path), '/');
$this->gravParams = $parts['params'];
} else {
$this->route = trim(RouteFactory::stripParams($path, true), '/');
$this->gravParams = RouteFactory::getParams($path);
}
if (isset($parts['query'])) {
$this->queryParams = UriFactory::parseQuery($parts['query']);
}
}
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* @package Grav\Framework\Route
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Route;
/**
* Class RouteFactory
* @package Grav\Framework\Route
*/
class RouteFactory
{
/** @var string */
private static $root = '';
/** @var string */
private static $language = '';
/** @var string */
private static $delimiter = ':';
public static function createFromParts($parts)
{
return new Route($parts);
}
public static function createFromString($path)
{
$path = ltrim($path, '/');
$parts = [
'path' => $path,
'query' => '',
'query_params' => [],
'grav' => [
'root' => self::$root,
'language' => self::$language,
'route' => $path,
'params' => ''
],
];
return new Route($parts);
}
public static function getRoot()
{
return self::$root;
}
public static function setRoot($root)
{
self::$root = rtrim($root, '/');
}
public static function getLanguage()
{
return self::$language;
}
public static function setLanguage($language)
{
self::$language = trim($language, '/');
}
public static function getParamValueDelimiter()
{
return self::$delimiter;
}
public static function setParamValueDelimiter($delimiter)
{
self::$delimiter = $delimiter;
}
/**
* @param array $params
* @return string
*/
public static function buildParams(array $params)
{
if (!$params) {
return '';
}
$delimiter = self::$delimiter;
$output = [];
foreach ($params as $key => $value) {
$output[] = "{$key}{$delimiter}{$value}";
}
return implode('/', $output);
}
/**
* @param string $path
* @param bool $decode
* @return string
*/
public static function stripParams($path, $decode = false)
{
$pos = strpos($path, self::$delimiter);
if ($pos === false) {
return $path;
}
$path = dirname(substr($path, 0, $pos));
if ($path === '.') {
return '';
}
return $decode ? rawurldecode($path) : $path;
}
/**
* @param string $path
* @return array
*/
public static function getParams($path)
{
$params = ltrim(substr($path, strlen(static::stripParams($path))), '/');
return $params !== '' ? static::parseParams($params) : [];
}
/**
* @param string $str
* @return array
*/
public static function parseParams($str)
{
$delimiter = self::$delimiter;
$params = explode('/', $str);
foreach ($params as &$param) {
$parts = explode($delimiter, $param, 2);
if (isset($parts[1])) {
$param[rawurldecode($parts[0])] = rawurldecode($parts[1]);
}
}
return $params;
}
}

View File

@@ -0,0 +1,345 @@
<?php
/**
* @package Grav\Framework\Session
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Session;
/**
* Class Session
* @package Grav\Framework\Session
*/
class Session implements SessionInterface
{
protected $options;
/**
* @var bool
*/
protected $started = false;
/**
* @var Session
*/
protected static $instance;
/**
* @inheritdoc
*/
public static function getInstance()
{
if (null === self::$instance) {
throw new \RuntimeException("Session hasn't been initialized.", 500);
}
return self::$instance;
}
public function __construct(array $options = [])
{
// Session is a singleton.
if (\PHP_SAPI === 'cli') {
self::$instance = $this;
return;
}
if (null !== self::$instance) {
throw new \RuntimeException('Session has already been initialized.', 500);
}
// Destroy any existing sessions started with session.auto_start
if ($this->isSessionStarted()) {
session_unset();
session_destroy();
}
// Set default options.
$options += array(
'cache_limiter' => 'nocache',
'use_trans_sid' => 0,
'use_cookies' => 1,
'lazy_write' => 1,
'use_strict_mode' => 1
);
$this->setOptions($options);
session_register_shutdown();
self::$instance = $this;
}
/**
* @inheritdoc
*/
public function getId()
{
return session_id();
}
/**
* @inheritdoc
*/
public function setId($id)
{
session_id($id);
return $this;
}
/**
* @inheritdoc
*/
public function getName()
{
return session_name();
}
/**
* @inheritdoc
*/
public function setName($name)
{
session_name($name);
return $this;
}
/**
* @inheritdoc
*/
public function setOptions(array $options)
{
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
return;
}
$allowedOptions = [
'save_path' => true,
'name' => true,
'save_handler' => true,
'gc_probability' => true,
'gc_divisor' => true,
'gc_maxlifetime' => true,
'serialize_handler' => true,
'cookie_lifetime' => true,
'cookie_path' => true,
'cookie_domain' => true,
'cookie_secure' => true,
'cookie_httponly' => true,
'use_strict_mode' => true,
'use_cookies' => true,
'use_only_cookies' => true,
'referer_check' => true,
'cache_limiter' => true,
'cache_expire' => true,
'use_trans_sid' => true,
'trans_sid_tags' => true, // PHP 7.1
'trans_sid_hosts' => true, // PHP 7.1
'sid_length' => true, // PHP 7.1
'sid_bits_per_character' => true, // PHP 7.1
'upload_progress.enabled' => true,
'upload_progress.cleanup' => true,
'upload_progress.prefix' => true,
'upload_progress.name' => true,
'upload_progress.freq' => true,
'upload_progress.min-freq' => true,
'lazy_write' => true,
'url_rewriter.tags' => true, // Not used in PHP 7.1
'hash_function' => true, // Not used in PHP 7.1
'hash_bits_per_character' => true, // Not used in PHP 7.1
'entropy_file' => true, // Not used in PHP 7.1
'entropy_length' => true, // Not used in PHP 7.1
];
foreach ($options as $key => $value) {
if (is_array($value)) {
// Allow nested options.
foreach ($value as $key2 => $value2) {
$ckey = "{$key}.{$key2}";
if (isset($value2, $allowedOptions[$ckey])) {
$this->ini_set("session.{$ckey}", $value2);
}
}
} elseif (isset($value, $allowedOptions[$key])) {
$this->ini_set("session.{$key}", $value);
}
}
}
/**
* @inheritdoc
*/
public function start($readonly = false)
{
// Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836
if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) {
unset($_COOKIE[session_name()]);
}
$options = $this->options;
if ($readonly) {
$options['read_and_close'] = '1';
}
$success = @session_start($options);
if (!$success) {
$last = error_get_last();
$error = $last ? $last['message'] : 'Unknown error';
throw new \RuntimeException('Failed to start session: ' . $error, 500);
}
$params = session_get_cookie_params();
setcookie(
session_name(),
session_id(),
time() + $params['lifetime'],
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
$this->started = true;
return $this;
}
/**
* @inheritdoc
*/
public function invalidate()
{
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
if ($this->isSessionStarted()) {
session_unset();
session_destroy();
}
$this->started = false;
return $this;
}
/**
* @inheritdoc
*/
public function close()
{
if ($this->started) {
session_write_close();
}
$this->started = false;
return $this;
}
/**
* @inheritdoc
*/
public function clear()
{
session_unset();
return $this;
}
/**
* @inheritdoc
*/
public function getAll()
{
return $_SESSION;
}
/**
* @inheritdoc
*/
public function getIterator()
{
return new \ArrayIterator($_SESSION);
}
/**
* @inheritdoc
*/
public function isStarted()
{
return $this->started;
}
/**
* @inheritdoc
*/
public function __isset($name)
{
return isset($_SESSION[$name]);
}
/**
* @inheritdoc
*/
public function __get($name)
{
return isset($_SESSION[$name]) ? $_SESSION[$name] : null;
}
/**
* @inheritdoc
*/
public function __set($name, $value)
{
$_SESSION[$name] = $value;
}
/**
* @inheritdoc
*/
public function __unset($name)
{
unset($_SESSION[$name]);
}
/**
* http://php.net/manual/en/function.session-status.php#113468
* Check if session is started nicely.
* @return bool
*/
protected function isSessionStarted()
{
return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false;
}
/**
* @param string $key
* @param mixed $value
*/
protected function ini_set($key, $value)
{
if (!is_string($value)) {
if (is_bool($value)) {
$value = $value ? '1' : '0';
}
$value = (string)$value;
}
$this->options[$key] = $value;
ini_set($key, $value);
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* @package Grav\Framework\Session
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Session;
/**
* Class Session
* @package Grav\Framework\Session
*/
interface SessionInterface extends \IteratorAggregate
{
/**
* Get current session instance.
*
* @return Session
* @throws \RuntimeException
*/
public static function getInstance();
/**
* Get session ID
*
* @return string|null Session ID
*/
public function getId();
/**
* Set session ID
*
* @param string $id Session ID
*
* @return $this
*/
public function setId($id);
/**
* Get session name
*
* @return string|null
*/
public function getName();
/**
* Set session name
*
* @param string $name
*
* @return $this
*/
public function setName($name);
/**
* Sets session.* ini variables.
*
* @param array $options
*
* @see http://php.net/session.configuration
*/
public function setOptions(array $options);
/**
* Starts the session storage
*
* @param bool $readonly
* @return $this
* @throws \RuntimeException
*/
public function start($readonly = false);
/**
* Invalidates the current session.
*
* @return $this
*/
public function invalidate();
/**
* Force the session to be saved and closed
*
* @return $this
*/
public function close();
/**
* Free all session variables.
*
* @return $this
*/
public function clear();
/**
* Returns all session variables.
*
* @return array
*/
public function getAll();
/**
* Retrieve an external iterator
*
* @return \ArrayIterator Return an ArrayIterator of $_SESSION
*/
public function getIterator();
/**
* Checks if the session was started.
*
* @return Boolean
*/
public function isStarted();
/**
* Checks if session variable is defined.
*
* @param string $name
* @return bool
*/
public function __isset($name);
/**
* Returns session variable.
*
* @param string $name
* @return mixed
*/
public function __get($name);
/**
* Sets session variable.
*
* @param string $name
* @param mixed $value
*/
public function __set($name, $value);
/**
* Removes session variable.
*
* @param string $name
*/
public function __unset($name);
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* @package Grav\Framework\Uri
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Uri;
use Grav\Framework\Psr7\AbstractUri;
use GuzzleHttp\Psr7\Uri as GuzzleUri;
use Psr\Http\Message\UriInterface;
/**
* Implements PSR-7 UriInterface.
*
* @package Grav\Framework\Uri
*/
class Uri extends AbstractUri
{
/** @var array Array of Uri query. */
private $queryParams;
/**
* You can use `UriFactory` functions to create new `Uri` objects.
*
* @param array $parts
* @throws \InvalidArgumentException
*/
public function __construct(array $parts = [])
{
$this->initParts($parts);
}
/**
* @return string
*/
public function getUser()
{
return parent::getUser();
}
/**
* @return string
*/
public function getPassword()
{
return parent::getPassword();
}
/**
* @return array
*/
public function getParts()
{
return parent::getParts();
}
/**
* @return string
*/
public function getUrl()
{
return parent::getUrl();
}
/**
* @return string
*/
public function getBaseUrl()
{
return parent::getBaseUrl();
}
/**
* @param string $key
* @return string|null
*/
public function getQueryParam($key)
{
$queryParams = $this->getQueryParams();
return isset($queryParams[$key]) ? $queryParams[$key] : null;
}
/**
* @param string $key
* @return UriInterface
*/
public function withoutQueryParam($key)
{
return GuzzleUri::withoutQueryValue($this, $key);
}
/**
* @param string $key
* @param string|null $value
* @return UriInterface
*/
public function withQueryParam($key, $value)
{
return GuzzleUri::withQueryValue($this, $key, $value);
}
/**
* @return array
*/
public function getQueryParams()
{
if ($this->queryParams === null) {
$this->queryParams = UriFactory::parseQuery($this->getQuery());
}
return $this->queryParams;
}
/**
* @param array $params
* @return UriInterface
*/
public function withQueryParams(array $params)
{
$query = UriFactory::buildQuery($params);
return $this->withQuery($query);
}
/**
* Whether the URI has the default port of the current scheme.
*
* `$uri->getPort()` may return the standard port. This method can be used for some non-http/https Uri.
*
* @return bool
*/
public function isDefaultPort()
{
return $this->getPort() === null || GuzzleUri::isDefaultPort($this);
}
/**
* Whether the URI is absolute, i.e. it has a scheme.
*
* An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
* if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
* to another URI, the base URI. Relative references can be divided into several forms:
* - network-path references, e.g. '//example.com/path'
* - absolute-path references, e.g. '/path'
* - relative-path references, e.g. 'subpath'
*
* @return bool
* @link https://tools.ietf.org/html/rfc3986#section-4
*/
public function isAbsolute()
{
return GuzzleUri::isAbsolute($this);
}
/**
* Whether the URI is a network-path reference.
*
* A relative reference that begins with two slash characters is termed an network-path reference.
*
* @return bool
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public function isNetworkPathReference()
{
return GuzzleUri::isNetworkPathReference($this);
}
/**
* Whether the URI is a absolute-path reference.
*
* A relative reference that begins with a single slash character is termed an absolute-path reference.
*
* @return bool
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public function isAbsolutePathReference()
{
return GuzzleUri::isAbsolutePathReference($this);
}
/**
* Whether the URI is a relative-path reference.
*
* A relative reference that does not begin with a slash character is termed a relative-path reference.
*
* @return bool
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public function isRelativePathReference()
{
return GuzzleUri::isRelativePathReference($this);
}
/**
* Whether the URI is a same-document reference.
*
* A same-document reference refers to a URI that is, aside from its fragment
* component, identical to the base URI. When no base URI is given, only an empty
* URI reference (apart from its fragment) is considered a same-document reference.
*
* @param UriInterface|null $base An optional base URI to compare against
* @return bool
* @link https://tools.ietf.org/html/rfc3986#section-4.4
*/
public function isSameDocumentReference(UriInterface $base = null)
{
return GuzzleUri::isSameDocumentReference($this, $base);
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* @package Grav\Framework\Uri
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Uri;
/**
* Class Uri
* @package Grav\Framework\Uri
*/
class UriFactory
{
/**
* @param array $env
* @return Uri
* @throws \InvalidArgumentException
*/
public static function createFromEnvironment(array $env)
{
return new Uri(static::parseUrlFromEnvironment($env));
}
/**
* @param string $uri
* @return Uri
* @throws \InvalidArgumentException
*/
public static function createFromString($uri)
{
return new Uri(static::parseUrl($uri));
}
/**
* Creates a URI from a array of `parse_url()` components.
*
* @param array $parts
* @return Uri
* @throws \InvalidArgumentException
*/
public static function createFromParts(array $parts)
{
return new Uri($parts);
}
/**
* @param array $env
* @return array
* @throws \InvalidArgumentException
*/
public static function parseUrlFromEnvironment(array $env)
{
// Build scheme.
if (isset($env['REQUEST_SCHEME'])) {
$scheme = strtolower($env['REQUEST_SCHEME']);
} else {
$https = isset($env['HTTPS']) ? $env['HTTPS'] : '';
$scheme = (empty($https) || strtolower($https) === 'off') ? 'http' : 'https';
}
// Build user and password.
$user = isset($env['PHP_AUTH_USER']) ? $env['PHP_AUTH_USER'] : '';
$pass = isset($env['PHP_AUTH_PW']) ? $env['PHP_AUTH_PW'] : '';
// Build host.
$host = 'localhost';
if (isset($env['HTTP_HOST'])) {
$host = $env['HTTP_HOST'];
} elseif (isset($env['SERVER_NAME'])) {
$host = $env['SERVER_NAME'];
}
// Remove port from HTTP_HOST generated $hostname
$host = explode(':', $host)[0];
// Build port.
$port = isset($env['SERVER_PORT']) ? (int)$env['SERVER_PORT'] : null;
// Build path.
$request_uri = isset($env['REQUEST_URI']) ? $env['REQUEST_URI'] : '';
$path = parse_url('http://example.com' . $request_uri, PHP_URL_PATH);
// Build query string.
$query = isset($env['QUERY_STRING']) ? $env['QUERY_STRING'] : '';
if ($query === '') {
$query = parse_url('http://example.com' . $request_uri, PHP_URL_QUERY);
}
// Support ngnix routes.
if (strpos($query, '_url=') === 0) {
parse_str($query, $q);
unset($q['_url']);
$query = http_build_query($q);
}
return [
'scheme' => $scheme,
'user' => $user,
'pass' => $pass,
'host' => $host,
'port' => $port,
'path' => $path,
'query' => $query
];
}
/**
* UTF-8 aware parse_url() implementation.
*
* @param string $url
* @return array
* @throws \InvalidArgumentException
*/
public static function parseUrl($url)
{
if (!is_string($url)) {
throw new \InvalidArgumentException('URL must be a string');
}
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%u',
function ($matches) { return rawurlencode($matches[0]); },
$url
);
$parts = parse_url($encodedUrl);
if ($parts === false) {
throw new \InvalidArgumentException('Malformed URL: ' . $encodedUrl);
}
return $parts;
}
/**
* Parse query string and return it as an array.
*
* @param string $query
* @return mixed
*/
public static function parseQuery($query)
{
parse_str($query, $params);
return $params;
}
/**
* Build query string from variables.
*
* @param array $params
* @return string
*/
public static function buildQuery(array $params)
{
return $params ? http_build_query($params, null, ini_get('arg_separator.output'), PHP_QUERY_RFC3986) : '';
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* @package Grav\Framework\Uri
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Uri;
/**
* Class Uri
* @package Grav\Framework\Uri
*/
class UriPartsFilter
{
const HOSTNAME_REGEX = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/u';
/**
* @param string $scheme
* @return string
* @throws \InvalidArgumentException If the scheme is invalid.
*/
public static function filterScheme($scheme)
{
if (!is_string($scheme)) {
throw new \InvalidArgumentException('Uri scheme must be a string');
}
return strtolower($scheme);
}
/**
* Filters the user info string.
*
* @param string $info The raw user or password.
* @return string The percent-encoded user or password string.
* @throws \InvalidArgumentException
*/
public static function filterUserInfo($info)
{
if (!is_string($info)) {
throw new \InvalidArgumentException('Uri user info must be a string');
}
return preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=]+|%(?![A-Fa-f0-9]{2}))/u',
function ($match) {
return rawurlencode($match[0]);
},
$info
);
}
/**
* @param string $host
* @return string
* @throws \InvalidArgumentException If the host is invalid.
*/
public static function filterHost($host)
{
if (!is_string($host)) {
throw new \InvalidArgumentException('Uri host must be a string');
}
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$host = '[' . $host . ']';
} elseif ($host && preg_match(static::HOSTNAME_REGEX, $host) !== 1) {
throw new \InvalidArgumentException('Uri host name validation failed');
}
return strtolower($host);
}
/**
* Filter Uri port.
*
* This method
*
* @param int|null $port
* @return int|null
* @throws \InvalidArgumentException If the port is invalid.
*/
public static function filterPort($port = null)
{
if (null === $port || (is_int($port) && ($port >= 1 && $port <= 65535))) {
return $port;
}
throw new \InvalidArgumentException('Uri port must be null or an integer between 1 and 65535');
}
/**
* Filter Uri path.
*
* This method percent-encodes all reserved characters in the provided path string. This method
* will NOT double-encode characters that are already percent-encoded.
*
* @param string $path The raw uri path.
* @return string The RFC 3986 percent-encoded uri path.
* @throws \InvalidArgumentException If the path is invalid.
* @link http://www.faqs.org/rfcs/rfc3986.html
*/
public static function filterPath($path)
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Uri path must be a string');
}
return preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
function ($match) {
return rawurlencode($match[0]);
},
$path
);
}
/**
* Filters the query string or fragment of a URI.
*
* @param string $query The raw uri query string.
* @return string The percent-encoded query string.
* @throws \InvalidArgumentException If the query is invalid.
*/
public static function filterQueryOrFragment($query)
{
if (!is_string($query)) {
throw new \InvalidArgumentException('Uri query string and fragment must be a string');
}
return preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
function ($match) {
return rawurlencode($match[0]);
},
$query
);
}
}