diff --git a/CHANGELOG.md b/CHANGELOG.md index 58cab13dd..1177bd10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# v2.0.0-dev +## mm/dd/2017 + +1. [](#new) + * 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 + * Added `Grav\Framework\Page` interfaces + * Deprecated GravTrait + # v1.2.5 ## mm/dd/2017 diff --git a/composer.lock b/composer.lock index 0c876ab34..28474e547 100644 --- a/composer.lock +++ b/composer.lock @@ -967,16 +967,16 @@ }, { "name": "symfony/console", - "version": "v2.8.19", + "version": "v2.8.20", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "86407ff20855a5eaa2a7219bd815e9c40a88633e" + "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/86407ff20855a5eaa2a7219bd815e9c40a88633e", - "reference": "86407ff20855a5eaa2a7219bd815e9c40a88633e", + "url": "https://api.github.com/repos/symfony/console/zipball/2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e", + "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e", "shasum": "" }, "require": { @@ -1024,7 +1024,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-04-03T20:37:06+00:00" + "time": "2017-04-26T01:38:53+00:00" }, { "name": "symfony/debug", @@ -1085,16 +1085,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.19", + "version": "v2.8.20", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "88b65f0ac25355090e524aba4ceb066025df8bd2" + "reference": "7fc8e2b4118ff316550596357325dfd92a51f531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/88b65f0ac25355090e524aba4ceb066025df8bd2", - "reference": "88b65f0ac25355090e524aba4ceb066025df8bd2", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7fc8e2b4118ff316550596357325dfd92a51f531", + "reference": "7fc8e2b4118ff316550596357325dfd92a51f531", "shasum": "" }, "require": { @@ -1141,7 +1141,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-04-03T20:37:06+00:00" + "time": "2017-04-26T16:56:54+00:00" }, { "name": "symfony/polyfill-iconv", @@ -1263,16 +1263,16 @@ }, { "name": "symfony/var-dumper", - "version": "v2.8.19", + "version": "v2.8.20", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "f8ff23ad5352f96e66c1df5468d492d2f37f3ac4" + "reference": "18ab1b833d2d82eb40a707bc002cbe62a1c22d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f8ff23ad5352f96e66c1df5468d492d2f37f3ac4", - "reference": "f8ff23ad5352f96e66c1df5468d492d2f37f3ac4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/18ab1b833d2d82eb40a707bc002cbe62a1c22d0b", + "reference": "18ab1b833d2d82eb40a707bc002cbe62a1c22d0b", "shasum": "" }, "require": { @@ -1283,9 +1283,11 @@ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" }, "require-dev": { + "ext-iconv": "*", "twig/twig": "~1.20|~2.0" }, "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", "ext-symfony_debug": "" }, "type": "library", @@ -1325,20 +1327,20 @@ "debug", "dump" ], - "time": "2017-03-12T16:01:59+00:00" + "time": "2017-04-28T06:26:40+00:00" }, { "name": "symfony/yaml", - "version": "v2.8.19", + "version": "v2.8.20", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "286d84891690b0e2515874717e49360d1c98a703" + "reference": "93ccdde79f4b079c7558da4656a3cb1c50c68e02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/286d84891690b0e2515874717e49360d1c98a703", - "reference": "286d84891690b0e2515874717e49360d1c98a703", + "url": "https://api.github.com/repos/symfony/yaml/zipball/93ccdde79f4b079c7558da4656a3cb1c50c68e02", + "reference": "93ccdde79f4b079c7558da4656a3cb1c50c68e02", "shasum": "" }, "require": { @@ -1374,7 +1376,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-03-20T09:41:44+00:00" + "time": "2017-05-01T14:31:55+00:00" }, { "name": "twig/twig", @@ -1648,16 +1650,16 @@ }, { "name": "facebook/webdriver", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/facebook/php-webdriver.git", - "reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2" + "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/3ea034c056189e11c0ce7985332a9f4b5b2b5db2", - "reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83", + "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83", "shasum": "" }, "require": { @@ -1696,7 +1698,7 @@ "selenium", "webdriver" ], - "time": "2017-03-22T10:56:03+00:00" + "time": "2017-04-28T14:54:49+00:00" }, { "name": "fzaninotto/faker", @@ -2978,16 +2980,16 @@ }, { "name": "symfony/browser-kit", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "2fe0caa60c1a1dfeefd0425741182687a9b382b8" + "reference": "9fab1ab6f77b77f3df5fc5250fc6956811699b57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/2fe0caa60c1a1dfeefd0425741182687a9b382b8", - "reference": "2fe0caa60c1a1dfeefd0425741182687a9b382b8", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/9fab1ab6f77b77f3df5fc5250fc6956811699b57", + "reference": "9fab1ab6f77b77f3df5fc5250fc6956811699b57", "shasum": "" }, "require": { @@ -3031,20 +3033,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2017-02-21T09:12:04+00:00" + "time": "2017-04-12T14:13:17+00:00" }, { "name": "symfony/css-selector", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c" + "reference": "02983c144038e697c959e6b06ef6666de759ccbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/a48f13dc83c168f1253a5d2a5a4fb46c36244c4c", - "reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc", + "reference": "02983c144038e697c959e6b06ef6666de759ccbc", "shasum": "" }, "require": { @@ -3084,20 +3086,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2017-02-21T09:12:04+00:00" + "time": "2017-05-01T14:55:58+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "403944e294cf4ceb3b8447f54cbad88ea7b99cee" + "reference": "f1ad34e8af09ed17570e027cf0c58a12eddec286" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/403944e294cf4ceb3b8447f54cbad88ea7b99cee", - "reference": "403944e294cf4ceb3b8447f54cbad88ea7b99cee", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/f1ad34e8af09ed17570e027cf0c58a12eddec286", + "reference": "f1ad34e8af09ed17570e027cf0c58a12eddec286", "shasum": "" }, "require": { @@ -3140,20 +3142,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2017-02-21T09:12:04+00:00" + "time": "2017-04-12T14:13:17+00:00" }, { "name": "symfony/finder", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "b20900ce5ea164cd9314af52725b0bb5a758217a" + "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/b20900ce5ea164cd9314af52725b0bb5a758217a", - "reference": "b20900ce5ea164cd9314af52725b0bb5a758217a", + "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", + "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", "shasum": "" }, "require": { @@ -3189,20 +3191,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-03-20T09:32:19+00:00" + "time": "2017-04-12T14:13:17+00:00" }, { "name": "symfony/process", - "version": "v3.2.7", + "version": "v3.2.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "57fdaa55827ae14d617550ebe71a820f0a5e2282" + "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/57fdaa55827ae14d617550ebe71a820f0a5e2282", - "reference": "57fdaa55827ae14d617550ebe71a820f0a5e2282", + "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", "shasum": "" }, "require": { @@ -3238,7 +3240,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-03-27T18:07:02+00:00" + "time": "2017-04-12T14:13:17+00:00" }, { "name": "webmozart/assert", diff --git a/system/src/Grav/Common/GravTrait.php b/system/src/Grav/Common/GravTrait.php index 36692d6cc..99848d224 100644 --- a/system/src/Grav/Common/GravTrait.php +++ b/system/src/Grav/Common/GravTrait.php @@ -8,6 +8,9 @@ namespace Grav\Common; +/** + * @deprecated in Grav 2.0 + */ trait GravTrait { protected static $grav; @@ -21,7 +24,9 @@ public static function getGrav() 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/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 @@ +