diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0dca053..2400b8ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer.json b/composer.json index d1e846396..1f299804e 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 69a7a8267..0c876ab34 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php index 6d1bf8e3d..679772306 100644 --- a/system/src/Grav/Common/Debugger.php +++ b/system/src/Grav/Common/Debugger.php @@ -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 * diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 456ed4189..7d239018d 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -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); } /** diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 0640ee859..c2ccfcb5a 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -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']); } diff --git a/system/src/Grav/Common/GravTrait.php b/system/src/Grav/Common/GravTrait.php index 46e1c20b6..afc08f6c0 100644 --- a/system/src/Grav/Common/GravTrait.php +++ b/system/src/Grav/Common/GravTrait.php @@ -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; } } diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index a5902d0d8..d3b5f86b2 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -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. */ diff --git a/system/src/Grav/Common/Processors/ConfigurationProcessor.php b/system/src/Grav/Common/Processors/ConfigurationProcessor.php index 84a8c3178..ed3adcd05 100644 --- a/system/src/Grav/Common/Processors/ConfigurationProcessor.php +++ b/system/src/Grav/Common/Processors/ConfigurationProcessor.php @@ -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(); } } diff --git a/system/src/Grav/Common/Processors/ProcessorBase.php b/system/src/Grav/Common/Processors/ProcessorBase.php index 45ba01ccc..c53a989c7 100644 --- a/system/src/Grav/Common/Processors/ProcessorBase.php +++ b/system/src/Grav/Common/Processors/ProcessorBase.php @@ -10,7 +10,7 @@ namespace Grav\Common\Processors; use Grav\Common\Grav; -class ProcessorBase +abstract class ProcessorBase { /** * @var Grav diff --git a/system/src/Grav/Common/Processors/RenderProcessor.php b/system/src/Grav/Common/Processors/RenderProcessor.php index 55e05a15e..623be38e6 100644 --- a/system/src/Grav/Common/Processors/RenderProcessor.php +++ b/system/src/Grav/Common/Processors/RenderProcessor.php @@ -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'); } } diff --git a/system/src/Grav/Common/Service/OutputServiceProvider.php b/system/src/Grav/Common/Service/OutputServiceProvider.php index 877fd6e3e..92871fd2a 100644 --- a/system/src/Grav/Common/Service/OutputServiceProvider.php +++ b/system/src/Grav/Common/Service/OutputServiceProvider.php @@ -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()); }; } } diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 98cde7337..2ba0b70d7 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -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 diff --git a/system/src/Grav/Common/User/Group.php b/system/src/Grav/Common/User/Group.php index 1472b07a9..f8bf2aca9 100644 --- a/system/src/Grav/Common/User/Group.php +++ b/system/src/Grav/Common/User/Group.php @@ -49,7 +49,7 @@ class Group extends Data * * @param string $groupname * - * @return object + * @return bool */ public static function groupExists($groupname) { diff --git a/system/src/Grav/Console/ConsoleTrait.php b/system/src/Grav/Console/ConsoleTrait.php index 4ffdafb27..138616240 100644 --- a/system/src/Grav/Console/ConsoleTrait.php +++ b/system/src/Grav/Console/ConsoleTrait.php @@ -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 */ diff --git a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php new file mode 100644 index 000000000..ad0830d94 --- /dev/null +++ b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php @@ -0,0 +1,53 @@ +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(); + } +} diff --git a/system/src/Grav/Framework/Collection/ArrayCollection.php b/system/src/Grav/Framework/Collection/ArrayCollection.php new file mode 100644 index 000000000..d123f8926 --- /dev/null +++ b/system/src/Grav/Framework/Collection/ArrayCollection.php @@ -0,0 +1,52 @@ +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(); + } +} diff --git a/system/src/Grav/Framework/Collection/CollectionInterface.php b/system/src/Grav/Framework/Collection/CollectionInterface.php new file mode 100644 index 000000000..ef46e00ec --- /dev/null +++ b/system/src/Grav/Framework/Collection/CollectionInterface.php @@ -0,0 +1,33 @@ +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() + ]; + } +} diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlock.php b/system/src/Grav/Framework/ContentBlock/ContentBlock.php new file mode 100644 index 000000000..c446c3a1b --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/ContentBlock.php @@ -0,0 +1,229 @@ +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)); + } + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php new file mode 100644 index 000000000..9412ef82f --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php @@ -0,0 +1,75 @@ +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); + } + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php new file mode 100644 index 000000000..08e4caad3 --- /dev/null +++ b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php @@ -0,0 +1,94 @@ +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'); +} diff --git a/system/src/Grav/Framework/Object/AbstractObject.php b/system/src/Grav/Framework/Object/AbstractObject.php new file mode 100644 index 000000000..0508162e2 --- /dev/null +++ b/system/src/Grav/Framework/Object/AbstractObject.php @@ -0,0 +1,185 @@ + 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); + } +} diff --git a/system/src/Grav/Framework/Object/AbstractObjectCollection.php b/system/src/Grav/Framework/Object/AbstractObjectCollection.php new file mode 100644 index 000000000..ecad694d9 --- /dev/null +++ b/system/src/Grav/Framework/Object/AbstractObjectCollection.php @@ -0,0 +1,35 @@ +id = $id; + } + + public function getId() + { + return $this->id ?: $this->getParentId(); + } +} diff --git a/system/src/Grav/Framework/Object/ObjectCollectionInterface.php b/system/src/Grav/Framework/Object/ObjectCollectionInterface.php new file mode 100644 index 000000000..ecacee917 --- /dev/null +++ b/system/src/Grav/Framework/Object/ObjectCollectionInterface.php @@ -0,0 +1,45 @@ + $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; + } +} diff --git a/system/src/Grav/Framework/Object/ObjectInterface.php b/system/src/Grav/Framework/Object/ObjectInterface.php new file mode 100644 index 000000000..b4ed09ef3 --- /dev/null +++ b/system/src/Grav/Framework/Object/ObjectInterface.php @@ -0,0 +1,62 @@ + (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.'); + } +} diff --git a/system/src/Grav/Framework/Object/Storage/FilesystemStorage.php b/system/src/Grav/Framework/Object/Storage/FilesystemStorage.php new file mode 100644 index 000000000..671827acc --- /dev/null +++ b/system/src/Grav/Framework/Object/Storage/FilesystemStorage.php @@ -0,0 +1,145 @@ +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)); + } +} diff --git a/system/src/Grav/Framework/Object/Storage/StorageInterface.php b/system/src/Grav/Framework/Object/Storage/StorageInterface.php new file mode 100644 index 000000000..e53cbcc8e --- /dev/null +++ b/system/src/Grav/Framework/Object/Storage/StorageInterface.php @@ -0,0 +1,47 @@ +