Merge branch 'feature/objects' of https://github.com/getgrav/grav into 2.0

This commit is contained in:
Matias Griese
2017-05-10 09:43:38 +03:00
32 changed files with 2479 additions and 36 deletions

View File

@@ -1,3 +1,18 @@
# Feature/Objects Branch
## mm/dd/2017
1. [](#new)
* Added `Pages::baseUrl()`, `Pages::homeUrl()` and `Pages::url()` functions
* Added `Debugger::getCaller()` to figure out where the method was called from
* Added support for custom output providers like Slim Framework
* Added `Grav\Framework\Collection` classes for creating collections
* Added `Grav\Framework\ContentBlock` classes which add better support for nested HTML blocks with assets
* Added `Grav\Framework\Object` classes to support general objects and their collections
* Deprecated GravTrait
1. [](#improved)
* Improve error handling in Folder::move()
* Added extra parameter for Twig::processSite() to include custom context
# v1.2.5
## 04/xx/2017

View File

@@ -16,6 +16,7 @@
"symfony/var-dumper": "~2.8",
"symfony/polyfill-iconv": "~1.0",
"doctrine/cache": "~1.5",
"doctrine/collections": "^1.4",
"filp/whoops": "~2.0",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.0",

69
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "e981b23b44354d081ec1e1ac214656a1",
"content-hash": "acef808d0660c4938b26a2348ca26191",
"packages": [
{
"name": "antoligy/dom-string-iterators",
@@ -120,6 +120,73 @@
],
"time": "2016-10-29T11:16:17+00:00"
},
{
"name": "doctrine/collections",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
"reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
"reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
},
"require-dev": {
"doctrine/coding-standard": "~0.1@dev",
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\Common\\Collections\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Collections Abstraction library",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"array",
"collections",
"iterator"
],
"time": "2017-01-03T10:49:41+00:00"
},
{
"name": "donatj/phpuseragentparser",
"version": "v0.7.0",

View File

@@ -120,6 +120,13 @@ class Debugger
return $this;
}
public function getCaller($ignore = 2)
{
$trace = debug_backtrace(false, $ignore);
return array_pop($trace);
}
/**
* Adds a data collector
*

View File

@@ -332,7 +332,8 @@ abstract class Folder
*/
public static function move($source, $target)
{
if (!is_dir($source)) {
if (!file_exists($source) || !is_dir($source)) {
// Rename fails if source folder does not exist.
throw new \RuntimeException('Cannot move non-existing folder.');
}
@@ -341,11 +342,24 @@ abstract class Folder
return;
}
if (file_exists($target)) {
// Rename fails if target folder exists.
throw new \RuntimeException('Cannot move files to existing folder/file.');
}
// Make sure that path to the target exists before moving.
self::create(dirname($target));
$success = @rename($source, $target);
if (!$success) {
// Silence warnings (chmod failed etc).
@rename($source, $target);
// Rename function can fail while still succeeding, so let's check if the folder exists.
if (!file_exists($target) || !is_dir($target)) {
// In some rare cases rename() creates file, not a folder. Get rid of it.
if (file_exists($target)) {
@unlink($target);
}
// Rename doesn't support moving folders across filesystems. Use copy instead.
self::copy($source, $target);
self::delete($source);
}
@@ -353,6 +367,7 @@ abstract class Folder
// Make sure that the change will be detected when caching.
@touch(dirname($source));
@touch(dirname($target));
@touch($target);
}
/**

View File

@@ -116,9 +116,6 @@ class Grav extends Container
*/
public function process()
{
/** @var Debugger $debugger */
$debugger = $this['debugger'];
// process all processors (e.g. config, initialize, assets, ..., render)
foreach ($this->processors as $processor) {
$processor = $this[$processor];
@@ -127,14 +124,10 @@ class Grav extends Container
});
}
// Set the header type
$this->header();
echo $this->output;
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$debugger->render();
$this->fireEvent('onOutputRendered');
register_shutdown_function([$this, 'shutdown']);
}

View File

@@ -8,6 +8,9 @@
namespace Grav\Common;
/**
* @deprecated Remove in Grav 2.0
*/
trait GravTrait
{
protected static $grav;
@@ -20,6 +23,10 @@ trait GravTrait
if (!self::$grav) {
self::$grav = Grav::instance();
}
$caller = self::$grav['debugger']->getCaller();
self::$grav['debugger']->addMessage("Deprecated GravTrait used in {$caller['file']}", 'deprecated');
return self::$grav;
}
}

View File

@@ -43,7 +43,12 @@ class Pages
/**
* @var string
*/
protected $base;
protected $base = '';
/**
* @var array|string[]
*/
protected $baseUrl = [];
/**
* @var array|string[]
@@ -100,7 +105,6 @@ class Pages
public function __construct(Grav $c)
{
$this->grav = $c;
$this->base = '';
}
/**
@@ -115,11 +119,82 @@ class Pages
if ($path !== null) {
$path = trim($path, '/');
$this->base = $path ? '/' . $path : null;
$this->baseUrl = [];
}
return $this->base;
}
/**
*
* Get base URL for Grav pages.
*
* @param string $lang Optional language code for multilingual links.
* @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
*
* @return string
*/
public function baseUrl($lang = null, $absolute = null)
{
$lang = (string) $lang;
$type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative');
$key = "{$lang} {$type}";
if (!isset($this->baseUrl[$key])) {
/** @var Config $config */
$config = $this->grav['config'];
/** @var Language $language */
$language = $this->grav['language'];
if (!$lang) {
$lang = $language->getActive();
}
$path_append = rtrim($this->grav['pages']->base(), '/');
if ($language->getDefault() != $lang || $config->get('system.languages.include_default_lang') === true) {
$path_append .= $lang ? '/' . $lang : '';
}
$this->baseUrl[$key] = $this->grav[$type] . $path_append;
}
return $this->baseUrl[$key];
}
/**
*
* Get home URL for Grav site.
*
* @param string $lang Optional language code for multilingual links.
* @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
*
* @return string
*/
public function homeUrl($lang = null, $absolute = null)
{
return $this->baseUrl($lang, $absolute) ?: '/';
}
/**
*
* Get home URL for Grav site.
*
* @param string $route Optional route to the page.
* @param string $lang Optional language code for multilingual links.
* @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
*
* @return string
*/
public function url($route = '/', $lang = null, $absolute = null)
{
if ($route === '/') {
return $this->homeUrl($lang, $absolute);
}
return $this->baseUrl($lang, $absolute) . $route;
}
/**
* Class initialization. Must be called before using this class.
*/

View File

@@ -16,6 +16,6 @@ class ConfigurationProcessor extends ProcessorBase implements ProcessorInterface
public function process()
{
$this->container['config']->init();
return $this->container['plugins']->setup();
$this->container['plugins']->setup();
}
}

View File

@@ -10,7 +10,7 @@ namespace Grav\Common\Processors;
use Grav\Common\Grav;
class ProcessorBase
abstract class ProcessorBase
{
/**
* @var Grav

View File

@@ -15,7 +15,22 @@ class RenderProcessor extends ProcessorBase implements ProcessorInterface
public function process()
{
$this->container->output = $this->container['output'];
$this->container->fireEvent('onOutputGenerated');
$container = $this->container;
$output = $container['output'];
if ($output instanceof \Psr\Http\Message\ResponseInterface) {
// Support for custom output providers like Slim Framework.
} else {
// Use internal Grav output.
$container->output = $output;
$container->fireEvent('onOutputGenerated');
// Set the header type
$container->header();
echo $output;
}
$container->fireEvent('onOutputRendered');
}
}

View File

@@ -8,6 +8,8 @@
namespace Grav\Common\Service;
use Grav\Common\Page\Page;
use Grav\Common\Twig\Twig;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
@@ -16,7 +18,13 @@ class OutputServiceProvider implements ServiceProviderInterface
public function register(Container $container)
{
$container['output'] = function ($c) {
return $c['twig']->processSite($c['page']->templateFormat());
/** @var Twig $twig */
$twig = $c['twig'];
/** @var Page $page */
$page = $c['page'];
return $twig->processSite($page->templateFormat());
};
}
}

View File

@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Language\Language;
use Grav\Common\Language\LanguageCodes;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Event\Event;
@@ -84,11 +85,6 @@ class Twig
$active_language = $language->getActive();
$path_append = rtrim($this->grav['pages']->base(), '/');
if ($language->getDefault() != $active_language || $config->get('system.languages.include_default_lang') === true) {
$path_append .= $active_language ? '/' . $active_language : '';
}
// handle language templates if available
if ($language->enabled()) {
$lang_templates = $locator->findResource('theme://templates/' . ($active_language ? $active_language : $language->getDefault()));
@@ -153,7 +149,8 @@ class Twig
$this->grav->fireEvent('onTwigExtensions');
$base_url = $this->grav['base_url'] . $path_append;
/** @var Pages $pages */
$pages = $this->grav['pages'];
// Set some standard variables for twig
$this->twig_vars = $this->twig_vars + [
@@ -166,11 +163,11 @@ class Twig
'taxonomy' => $this->grav['taxonomy'],
'browser' => $this->grav['browser'],
'base_dir' => rtrim(ROOT_DIR, '/'),
'base_url' => $base_url,
'home_url' => $pages->homeUrl($active_language),
'base_url' => $pages->baseUrl($active_language),
'base_url_absolute' => $pages->baseUrl($active_language, true),
'base_url_relative' => $pages->baseUrl($active_language, false),
'base_url_simple' => $this->grav['base_url'],
'base_url_absolute' => $this->grav['base_url_absolute'] . $path_append,
'base_url_relative' => $this->grav['base_url_relative'] . $path_append,
'home_url' => $base_url == '' ? '/' : $base_url,
'theme_dir' => $locator->findResource('theme://'),
'theme_url' => $this->grav['base_url'] . '/' . $locator->findResource('theme://', false),
'html_lang' => $this->grav['language']->getActive() ?: $config->get('site.default_lang', 'en'),
@@ -315,7 +312,7 @@ class Twig
* @return string the rendered output
* @throws \RuntimeException
*/
public function processSite($format = null)
public function processSite($format = null, array $vars = [])
{
// set the page now its been processed
$this->grav->fireEvent('onTwigSiteVariables');
@@ -343,7 +340,7 @@ class Twig
$template = $this->template($page->template() . $ext);
try {
$output = $this->twig->render($template, $twig_vars);
$output = $this->twig->render($template, $vars + $twig_vars);
} catch (\Twig_Error_Loader $e) {
$error_msg = $e->getMessage();
// Try html version of this template if initial template was NOT html

View File

@@ -49,7 +49,7 @@ class Group extends Data
*
* @param string $groupname
*
* @return object
* @return bool
*/
public static function groupExists($groupname)
{

View File

@@ -10,7 +10,6 @@ namespace Grav\Console;
use Grav\Common\Grav;
use Grav\Common\Composer;
use Grav\Common\GravTrait;
use Grav\Console\Cli\ClearCacheCommand;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\ArrayInput;
@@ -20,8 +19,6 @@ use Symfony\Component\Yaml\Yaml;
trait ConsoleTrait
{
use GravTrait;
/**
* @var
*/

View File

@@ -0,0 +1,53 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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 jsonSerialize()
{
$this->initialize();
return $this->collection->jsonSerialize();
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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()));
}
/**
* Implementes JsonSerializable interface.
*
* @return array
*/
public function jsonSerialize()
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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();
}

View File

@@ -0,0 +1,278 @@
<?php
/**
* @package Grav\Framework\Collection
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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 FileCollection extends AbstractLazyCollection
{
const INCLUDE_FILES = 1;
const INCLUDE_FOLDERS = 2;
const RECURSIVE = 4;
/**
* @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
* @param int $flags
*/
public function __construct($path, $flags = null)
{
$this->path = $path;
$this->flags = (int) ($flags ?: FileCollection::INCLUDE_FILES | FileCollection::INCLUDE_FOLDERS | FileCollection::RECURSIVE);
$this->setIterator();
$this->setFilter();
$this->setObjectBuilder();
$this->setNestingLimit();
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return int
*/
public function getFlags()
{
return $this->flags;
}
/**
* @return int
*/
public function getNestingLimit()
{
return $this->nestingLimit;
}
public 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|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)
{
if ($this->filterFunction) {
$oldFilterFunction = $this->filterFunction;
$this->filterFunction = function ($expr) use ($oldFilterFunction, $filterFunction) {
return $oldFilterFunction($expr) && $filterFunction($expr);
};
} else {
$this->filterFunction = $filterFunction;
}
return $this;
}
/**
* @param callable|null $objectFunction
* @return $this
*/
public function setObjectBuilder(callable $objectFunction = null)
{
$this->createObjectFunction = $objectFunction ?: [$this, 'createObject'];
return $this;
}
public function setNestingLimit($limit = 99)
{
$this->nestingLimit = (int) $limit;
return $this;
}
/**
* @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);
}
/**
* {@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,229 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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 = [];
/**
* @param string $id
* @return static
*/
public static function create($id = null)
{
return new static($id);
}
/**
* @param array $serialized
* @return ContentBlockInterface
*/
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 \RuntimeException('Bad data');
}
/** @var ContentBlockInterface $instance */
$instance = new $type($id);
$instance->build($serialized);
} catch (\Exception $e) {
throw new \RuntimeException(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->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
*/
public function build(array $serialized)
{
$this->checkVersion($serialized);
$this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
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 $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']) ? (string) $serialized['_version'] : 1;
if ($version != $this->version) {
throw new \RuntimeException(sprintf('Unsupported version %s', $version));
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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 $content
* @return $this
*/
public function setContent($content);
/**
* @param ContentBlockInterface $block
* @return $this
*/
public function addBlock(ContentBlockInterface $block);
}

View File

@@ -0,0 +1,374 @@
<?php
/**
* @package Grav\Framework\ContentBlock
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, 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
*/
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;
}
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) 2014 - 2017 RocketTheme, 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,185 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use RocketTheme\Toolbox\ArrayTraits\ArrayAccessWithGetters;
use RocketTheme\Toolbox\ArrayTraits\Export;
/**
* Abstract base class for stored objects.
*
* @property string $id
* @package Grav\Framework\Object
*/
abstract class AbstractObject implements ObjectInterface, StoredObjectInterface
{
use ObjectStorageTrait {
check as traitcheck;
}
use ArrayAccessWithGetters, Export;
/**
* Primary key for the object.
* @var array
*/
static protected $primaryKey = [
'id' => null
];
/**
* Default properties for the object.
* @var array
*/
static protected $defaults = [];
/**
* @var string
*/
static protected $collectionClass = 'Grav\\Framework\\Object\\AbstractObjectCollection';
/**
* Properties of the object.
* @var array
*/
protected $items;
/**
* @param array $ids List of primary Ids or null to return everything that has been loaded.
* @param bool $readonly
* @return AbstractObjectCollection
*/
static public function instances(array $ids = null, $readonly = true)
{
$collectionClass = static::$collectionClass;
if (is_null($ids)) {
return new $collectionClass(static::$instances);
}
if (empty($ids)) {
return new $collectionClass([]);
}
$results = [];
$list = [];
foreach ($ids as $id) {
if (!isset(static::$instances[$id])) {
$list[] = $id;
}
}
if ($list) {
$c = get_called_class();
$storage = static::getStorage();
$list = $storage->loadList($list);
foreach ($list as $keys) {
/** @var static $instance */
$instance = new $c();
$instance->doLoad($keys);
$id = $instance->getId();
if ($id && !isset(static::$instances[$id])) {
$instance->initialize();
static::$instances[$id] = $instance;
}
}
}
foreach ($ids as $id) {
if (isset(static::$instances[$id])) {
$results[$id] = $readonly ? clone static::$instances[$id] : static::$instances[$id];
}
}
return new $collectionClass($results);
}
/**
* Method to perform sanity checks on the instance properties to ensure they are safe to store in the storage.
*
* Child classes should override this method to make sure the data they are storing in the storage is safe and as
* expected before saving the object.
*
* @return bool True if the instance is sane and able to be stored in the storage.
*/
public function check($includeChildren = false)
{
return $this->checkKeys() && $this->traitCheck($includeChildren);
}
/**
* @param array $keys
* @return array
*/
public function getKeys(array $keys = [])
{
foreach (static::$primaryKey as $key => $value) {
if (!isset($keys[$key])) {
if (isset($this->items[$key])) {
$keys[$key] = $this->items[$key];
} else {
$keys[$key] = $value;
}
}
}
return $keys;
}
/**
* @param array $keys
* @return bool
*/
public function checkKeys(array $keys = [])
{
if (!$keys) {
$keys = $this->getKeys();
}
foreach ($keys as $key => $value) {
if ($value === null) {
return false;
}
}
return true;
}
/**
* Implementes JsonSerializable interface.
*
* @return array
*/
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Returns a string representation of this object.
*
* @return string
*/
public function __toString()
{
return __CLASS__ . '@' . spl_object_hash($this);
}
// Internal functions
/**
* @param array $items
* @param array $keys
*/
protected function doLoad(array $items, array $keys = [])
{
$this->items = array_replace(static::$defaults, $this->items, $this->getKeys($keys), $items);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use Grav\Framework\Collection\ArrayCollection;
/**
* Abstract Object Collection
* @package Grav\Framework\Object
*/
abstract class AbstractObjectCollection extends ArrayCollection implements ObjectCollectionInterface, StoredObjectInterface
{
use ObjectStorageTrait {
getId as getParentId;
}
use ObjectCollectionTrait;
protected $id;
public function setId($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id ?: $this->getParentId();
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use Grav\Framework\Collection\CollectionInterface;
/**
* ObjectCollection Interface
* @package Grav\Framework\Collection
*/
interface ObjectCollectionInterface extends CollectionInterface
{
/**
* Create a copy from this collection by cloning all objects in the collection.
*
* @return static
*/
public function copy();
/**
* @param string $property Object property to be fetched.
* @return array Values of the property.
*/
public function getProperty($property);
/**
* @param string $property Object property to be updated.
* @param string $value New value.
* @return $this
*/
public function setProperty($property, $value);
/**
* @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);
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
/**
* ObjectCollection Trait
* @package Grav\Framework\Object
*/
trait ObjectCollectionTrait
{
/**
* @param array $elements
* @return static
*/
abstract public function createFrom(array $elements);
/**
* Create a copy from this collection by cloning all objects in the collection.
*
* @return static
*/
public function copy()
{
$list = [];
foreach ($this as $key => $value) {
$list[$key] = is_object($value) ? clone $value : $value;
}
return $this->createFrom($list);
}
/**
* @param string $property Object property to be fetched.
* @return array Key/Value pairs of the properties.
*/
public function getProperty($property)
{
$list = [];
foreach ($this as $id => $element) {
$list[$id] = isset($element->{$property}) ? $element->{$property} : null;
}
return $list;
}
/**
* @param string $property Object property to be updated.
* @param string $value New value.
*/
public function setProperty($property, $value)
{
foreach ($this as $element) {
$element->{$property} = $value;
}
}
/**
* @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 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.
*
* @param string $property
* @return array
*/
public function group($property)
{
$list = [];
foreach ($this as $element) {
$list[$element->{$property}][] = $element;
}
return $list;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
/**
* Object Interface
* @package Grav\Framework\Object
*/
interface ObjectInterface extends \ArrayAccess, \JsonSerializable
{
/**
* Returns the global instance to the object.
*
* Note that using array of fields will always make a query, but it's very useful feature if you want to search one
* item by using arbitrary set of matching fields. If there are more than one matching object, first one gets returned.
*
* @param null|int|string|array $keys An optional primary key value to load the object by, or an array of fields to match.
* @param boolean $reload Force object to reload.
*
* @return Object
*/
static public function instance($keys = null, $reload = false);
/**
* @param array $ids List of primary Ids or null to return everything that has been loaded.
* @param bool $readonly
* @return AbstractObjectCollection
*/
static public function instances(array $ids = null, $readonly = true);
/**
* Removes all or selected instances from the object cache.
*
* @param null|int|string|array $ids An optional primary key or list of keys.
*/
static public function freeInstances($ids = null);
/**
* Override this function if you need to initialize object right after creating it.
*
* Can be used for example if the fields need to be converted from json strings to array.
*
* @return bool True if initialization was done, false if object was already initialized.
*/
public function initialize();
/**
* Method to perform sanity checks on the instance properties to ensure they are safe to store in the storage.
*
* Child classes should override this method to make sure the data they are storing in the storage is safe and as
* expected before saving the object.
*
* @return boolean True if the instance is sane and able to be stored in the storage.
*/
public function check();
}

View File

@@ -0,0 +1,385 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
use Grav\Framework\Object\Storage\StorageInterface;
/**
* Trait for implementing stored objects.
*
* @property string $id
* @package Grav\Framework\Object
*/
trait ObjectStorageTrait
{
/**
* If you don't have global instance ids, override this in extending class.
* @var array
*/
static protected $instances = [];
/**
* If you don't have global storage, override this in extending class.
* @var StorageInterface
*/
static protected $storage;
/**
* Readonly object.
* @var bool
*/
protected $readonly = false;
/**
* @var bool
*/
protected $initialized = false;
/**
* @param StorageInterface $storage
*/
static public function setStorage(StorageInterface $storage)
{
static::$storage = $storage;
}
/**
* Returns the global instance to the object.
*
* Note that using array of fields will always make a query, but it's very useful feature if you want to search one
* item by using arbitrary set of matching fields. If there are more than one matching object, first one gets returned.
*
* @param string|array $keys An optional primary key value to load the object by, or an array of fields to match.
* @param boolean $reload Force object to reload.
*
* @return ObjectInterface
*/
static public function instance($keys = null, $reload = false)
{
if (is_scalar($keys)) {
$keys = ['id' => (string) $keys];
}
$id = $keys ? static::getInstanceId($keys) : null;
// If we are creating or loading a new item or we load instance by alternative keys, we need to create a new object.
if (!$id || !isset(static::$instances[$id])) {
$c = get_called_class();
/** @var ObjectStorageTrait|ObjectInterface $instance */
$instance = new $c();
if (!$instance->load($keys)) {
return $instance;
}
// Instance exists in storage: make sure that we return the global instance.
$id = $instance->getId();
$reload = false;
}
// Return global instance from the identifier.
$instance = static::$instances[$id];
if ($reload) {
$instance->load();
}
return $instance;
}
/**
* Removes all or selected instances from the object cache.
*
* @param null|string|array $ids An optional primary key or list of keys.
*/
static public function freeInstances($ids = null)
{
if ($ids === null) {
$ids = array_keys(static::$instances);
}
$ids = (array) $ids;
foreach ($ids as $id) {
unset(static::$instances[$id]);
}
}
/**
* Override this function if you need to initialize object right after creating it.
*
* Can be used for example if the fields need to be converted from json strings to array.
*
* @return bool True if initialization was done, false if object was already initialized.
*/
public function initialize()
{
$initialized = $this->initialized;
$this->initialized = true;
return !$initialized;
}
/**
* Convert instance to a read only object.
*
* @return $this
*/
public function readonly()
{
$this->readonly = true;
return $this;
}
/**
* Returns true if the object has been stored.
*
* @return boolean True if object exists in storage.
*/
public function isSaved()
{
return $this->getStorage()->exists($this->getStorageKey());
}
/**
* Method to load object from the storage.
*
* @param mixed $keys An optional primary key value to load the object by, or an array of fields to match. If not
* set the instance key value is used.
* @param bool $getKey Internal parameter, please do not use.
*
* @return bool True on success, false if the object doesn't exist.
*/
public function load($keys = null, $getKey = true)
{
if ($getKey) {
if (is_scalar($keys)) {
$keys = ['id' => (string) $keys];
}
// Fetch internal key.
$key = $keys ? $this->getStorageKey($keys) : null;
} else {
// Internal key was passed.
$key = $keys;
$keys = [];
}
$this->doLoad($this->getStorage()->load($key), $keys);
$this->initialize();
$id = $this->getId();
if ($id) {
if (!isset(static::$instances[$id])) {
static::$instances[$id] = $this;
}
}
return $this->isSaved();
}
/**
* Method to save the object to the storage.
*
* Before saving the object, this method checks if object can be safely saved.
*
* @param bool $includeChildren
* @return bool True on success.
*/
public function save($includeChildren = false)
{
// Check the object.
if ($this->readonly || !$this->check($includeChildren) || !$this->onBeforeSave()) {
return false;
}
// Get storage.
$storage = $this->getStorage();
$key = $this->getStorageKey();
// Get data to be saved.
$data = $this->prepareSave();
// Save the object.
$exists = $storage->exists($key);
$id = $storage->save($key, $data);
if (!$id) {
throw new \LogicException('No id specified');
}
// If item was created, load the object (making sure it has been properly initialized).
if (!$exists) {
$this->load($id, false);
}
if ($includeChildren) {
$this->saveChildren();
}
$this->onAfterSave();
return true;
}
/**
* Method to delete the object from the database.
*
* @param bool $includeChildren
* @return bool True on success.
*/
public function delete($includeChildren = false)
{
if ($this->readonly || !$this->isSaved() || !$this->onBeforeDelete()) {
return false;
}
if ($includeChildren) {
$this->deleteChildren();
}
// Get storage.
$storage = $this->getStorage();
if (!$storage->delete($this->getStorageKey())) {
return false;
}
$this->onAfterDelete();
return true;
}
/**
* Method to perform sanity checks on the instance properties to ensure they are safe to store in the storage.
*
* Child classes should override this method to make sure the data they are storing in the storage is safe and as
* expected before saving the object.
*
* @return bool True if the instance is sane and able to be stored in the storage.
*/
public function check($includeChildren = false)
{
$result = true;
if ($includeChildren) {
foreach ($this->toArray() as $field => $value) {
if (is_object($value) && method_exists($value, 'check')) {
$result = $result && $value->check();
}
}
}
return $result;
}
// Internal functions
abstract protected function doLoad(array $items, array $keys = []);
/**
* @return bool
*/
protected function onBeforeSave()
{
return true;
}
protected function onAfterSave()
{
}
/**
* @return bool
*/
protected function onBeforeDelete()
{
return true;
}
protected function onAfterDelete()
{
}
protected function saveChildren()
{
foreach ($this->toArray() as $field => $value) {
if (is_object($value) && method_exists($value, 'save')) {
$value->save(true);
}
}
}
protected function deleteChildren()
{
foreach ($this->toArray() as $field => $value) {
if (is_object($value) && method_exists($value, 'delete')) {
$value->delete(true);
}
}
}
protected function prepareSave(array $data = null)
{
if ($data === null) {
$data = $this->toArray();
}
foreach ($data as $field => $value) {
if (is_object($value) && method_exists($value, 'save')) {
unset($data[$field]);
}
}
return $data;
}
/**
* Method to get the storage key for the object.
*
* @param array
* @return string
*/
abstract public function getStorageKey(array $keys = []);
/**
* @param array $keys
* @return string
*/
public function getInstanceId(array $keys)
{
return $this->getStorageKey($keys);
}
/**
* @return string
*/
public function getId()
{
return $this->getStorageKey();
}
/**
* @return StorageInterface
*/
protected static function getStorage()
{
if (!static::$storage) {
static::loadStorage();
}
return static::$storage;
}
protected static function loadStorage()
{
throw new \RuntimeException('Storage has not been set.');
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* @package Grav\Framework\Object\Storage
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Storage;
use Grav\Common\Grav;
use RocketTheme\Toolbox\File\FileInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* FilesystemStorage
* @package Grav\Framework\Object\Storage
*/
class FilesystemStorage implements StorageInterface
{
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $type = 'Grav\\Common\\File\\CompiledJsonFile';
/**
* @var string
*/
protected $extension = '.json';
/**
* @param string $path
* @param string $extension
* @param string $type
*/
public function __construct($path, $type = null, $extension = null)
{
$this->path = $path;
if ($type) {
$this->type = $type;
}
if ($extension) {
$this->extension = $extension;
}
}
/**
* @param string $key
* @return bool
*/
public function exists($key)
{
if ($key === null) {
return false;
}
$file = $this->getFile($key);
return $file->exists();
}
/**
* @param string $key
* @return array
*/
public function load($key)
{
if ($key === null) {
return [];
}
$file = $this->getFile($key);
$content = (array)$file->content();
$file->free();
return $content;
}
/**
* @param string $key
* @param array $data
* @return string
*/
public function save($key, array $data)
{
$file = $this->getFile($key);
$file->save($data);
$file->free();
return $key;
}
/**
* @param string $key
* @return bool
*/
public function delete($key)
{
$file = $this->getFile($key);
$result = $file->delete();
$file->free();
return $result;
}
/**
* @param string[] $list
* @return array
*/
public function loadList(array $list)
{
$results = [];
foreach ($list as $id) {
$results[$id] = $this->load($id);
}
return $results;
}
/**
* @param string $key
* @return FileInterface
*/
protected function getFile($key)
{
if ($key === null) {
throw new \RuntimeException('Storage key not defined');
}
$filename = "{$this->path}/{$key}{$this->extension}";
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
/** @var FileInterface $type */
$type = $this->type;
return $type::instance($locator->findResource($filename, true) ?: $locator->findResource($filename, true, true));
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* @package Grav\Framework\Object\Storage
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Storage;
/**
* Interface StorageInterface
* @package Grav\Framework\Object\Storage
*/
interface StorageInterface
{
/**
* @param string $key
* @return bool
*/
public function exists($key);
/**
* @param string $key
* @return array
*/
public function load($key);
/**
* @param string $key
* @param array $data
* @return string
*/
public function save($key, array $data);
/**
* @param string $key
* @return bool
*/
public function delete($key);
/**
* @param string[] $list
* @return array
*/
public function loadList(array $list);
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object;
/**
* Stored Object Interface
* @package Grav\Framework\Object
*/
interface StoredObjectInterface
{
/**
* Convert instance to a read only object.
*
* @return $this
*/
public function readonly();
/**
* Returns true if the object exists in the storage.
*
* @return boolean True if object exists.
*/
public function isSaved();
/**
* Method to load object from the storage.
*
* @param null|int|string|array $keys An optional primary key value to load the object by, or an array of fields
* to match. If not set the instance key value is used.
*
* @return boolean True on success, false if the object doesn't exist.
*/
public function load($keys = null);
/**
* Method to save the object to the storage.
*
* Before saving the object, this method checks if object can be safely saved.
*
* @param bool $includeChildren
* @return boolean True on success.
*/
public function save($includeChildren = false);
/**
* Method to delete the object from the database.
*
* @return boolean True on success.
*/
public function delete();
}