diff --git a/CHANGELOG.md b/CHANGELOG.md index 8590da83a..617449b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ 1. [](#new) * Refactor Data classes to use NestedArrayAccess instead of DataMutatorTrait * Data objects: Allow function call chaining - * Data objects: Lazy load blueprints only if needed + * Data objects: Lazy load blueprints only if needed + * Refactor Config classes # v1.0.0-rc.4 ## 10/29/2015 diff --git a/system/src/Grav/Common/Config/Blueprints.php b/system/src/Grav/Common/Config/Blueprints.php deleted file mode 100644 index 1af61f47c..000000000 --- a/system/src/Grav/Common/Config/Blueprints.php +++ /dev/null @@ -1,207 +0,0 @@ -grav = $grav ?: Grav::instance(); - } - - public function init() - { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - $blueprints = $locator->findResources('blueprints://config'); - $plugins = $locator->findResources('plugins://'); - - $blueprintFiles = $this->getBlueprintFiles($blueprints, $plugins); - - $this->loadCompiledBlueprints($plugins + $blueprints, $blueprintFiles); - } - - protected function loadCompiledBlueprints($blueprints, $blueprintFiles) - { - $checksum = md5(serialize($blueprints)); - $filename = CACHE_DIR . 'compiled/blueprints/' . $checksum .'.php'; - $checksum .= ':'.md5(serialize($blueprintFiles)); - $class = get_class($this); - $file = PhpFile::instance($filename); - - if ($file->exists()) { - $cache = $file->exists() ? $file->content() : null; - } else { - $cache = null; - } - - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || empty($cache['checksum']) - || empty($cache['$class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load blueprints. - $this->blueprints = new Blueprints(); - foreach ($blueprintFiles as $key => $files) { - $this->loadBlueprints($key); - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $blueprintFiles, - 'data' => $this->blueprints->toArray() - ]; - - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $file->save($cache); - $file->unlock(); - } - } else { - $this->blueprints = new Blueprints($cache['data']); - } - } - - /** - * Load global blueprints. - * - * @param string $key - * @param array $files - */ - public function loadBlueprints($key, array $files = null) - { - if (is_null($files)) { - $files = $this->files[$key]; - } - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->blueprints->embed($name, $file->content(), '/'); - } - } - - /** - * Get all blueprint files (including plugins). - * - * @param array $blueprints - * @param array $plugins - * @return array - */ - protected function getBlueprintFiles(array $blueprints, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectPlugins($folder, true); - } - foreach (array_reverse($blueprints) as $folder) { - $list += $this->detectConfig($folder, true); - } - return $list; - } - - /** - * Detects all plugins with a configuration file and returns last modification time. - * - * @param string $lookup Location to look up from. - * @param bool $blueprints - * @return array - * @internal - */ - protected function detectPlugins($lookup = SYSTEM_DIR, $blueprints = false) - { - $find = $blueprints ? 'blueprints.yaml' : '.yaml'; - $location = $blueprints ? 'blueprintFiles' : 'configFiles'; - $path = trim(Folder::getRelativePath($lookup), '/'); - if (isset($this->{$location}[$path])) { - return [$path => $this->{$location}[$path]]; - } - - $list = []; - - if (is_dir($lookup)) { - $iterator = new \DirectoryIterator($lookup); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir() || $directory->isDot()) { - continue; - } - - $name = $directory->getBasename(); - $filename = "{$path}/{$name}/" . ($find && $find[0] != '.' ? $find : $name . $find); - - if (is_file($filename)) { - $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - $this->{$location}[$path] = $list; - - return [$path => $list]; - } - - /** - * Detects all plugins with a configuration file and returns last modification time. - * - * @param string $lookup Location to look up from. - * @param bool $blueprints - * @return array - * @internal - */ - protected function detectConfig($lookup = SYSTEM_DIR, $blueprints = false) - { - $location = $blueprints ? 'blueprintFiles' : 'configFiles'; - $path = trim(Folder::getRelativePath($lookup), '/'); - if (isset($this->{$location}[$path])) { - return [$path => $this->{$location}[$path]]; - } - - if (is_dir($lookup)) { - // Find all system and user configuration files. - $options = [ - 'compare' => 'Filename', - 'pattern' => '|\.yaml$|', - 'filters' => [ - 'key' => '|\.yaml$|', - 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { - return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; - }], - 'key' => 'SubPathname' - ]; - - $list = Folder::all($lookup, $options); - } else { - $list = []; - } - - $this->{$location}[$path] = $list; - - return [$path => $list]; - } -} diff --git a/system/src/Grav/Common/Config/CompiledBase.php b/system/src/Grav/Common/Config/CompiledBase.php new file mode 100644 index 000000000..d20223ef1 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledBase.php @@ -0,0 +1,237 @@ +cacheFolder = $cacheFolder; + $this->files = $files; + $this->path = $path ? rtrim($path, '\\/') . '/' : ''; + } + + /** + * Get filename for the compiled PHP file. + * + * @param string $name + * @return $this + */ + public function name($name = null) + { + if (!$this->name) { + $this->name = $name ?: md5(json_encode(array_keys($this->files))); + } + + return $this; + } + + /** + * @return bool + */ + public function modified() + { + return $this->modified; + } + + /** + * Load the configuration. + * + * @return mixed + */ + public function load() + { + if ($this->object) { + return $this->object; + } + + $filename = $this->createFilename(); + if (!$this->loadCompiledFile($filename) && $this->loadFiles()) { + $this->saveCompiledFile($filename); + } + + return $this->object; + } + + /** + * Returns checksum from the configuration files. + * + * You can set $this->checksum = false to disable this check. + * + * @return bool|string + */ + public function checksum() + { + if (!isset($this->checksum)) { + $this->checksum = md5(json_encode($this->files) . $this->version); + } + + return $this->checksum; + } + + protected function createFilename() + { + return "{$this->cacheFolder}/{$this->name()->name}.php"; + } + + /** + * Create configuration object. + * + * @param array $data + */ + abstract protected function createObject(array $data = []); + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + abstract protected function loadFile($name, $filename); + + /** + * Load and join all configuration files. + * + * @return bool + * @internal + */ + protected function loadFiles() + { + $this->createObject(); + + $list = array_reverse($this->files); + foreach ($list as $files) { + foreach ($files as $name => $item) { + $this->loadFile($name, $this->path . $item['file']); + } + } + + return true; + } + + /** + * Load compiled file. + * + * @param string $filename + * @return bool + * @internal + */ + protected function loadCompiledFile($filename) + { + if (!file_exists($filename)) { + return false; + } + + $cache = include $filename; + if ( + !is_array($cache) + || !isset($cache['checksum']) + || !isset($cache['data']) + || !isset($cache['@class']) + || $cache['@class'] != get_class($this) + ) { + return false; + } + + // Load real file if cache isn't up to date (or is invalid). + if ($cache['checksum'] !== $this->checksum()) { + return false; + } + + $this->createObject($cache['data']); + + return true; + } + + /** + * Save compiled file. + * + * @param string $filename + * @throws \RuntimeException + * @internal + */ + protected function saveCompiledFile($filename) + { + $file = PhpFile::instance($filename); + + // Attempt to lock the file for writing. + try { + $file->lock(false); + } catch (\Exception $e) { + // Another process has locked the file; we will check this in a bit. + } + + if ($file->locked() === false) { + // File was already locked by another process. + return; + } + + $cache = [ + '@class' => get_class($this), + 'timestamp' => time(), + 'checksum' => $this->checksum(), + 'files' => $this->files, + 'data' => $this->object->toArray() + ]; + + $file->save($cache); + $file->unlock(); + $file->free(); + + $this->modified = true; + } +} diff --git a/system/src/Grav/Common/Config/CompiledBlueprints.php b/system/src/Grav/Common/Config/CompiledBlueprints.php new file mode 100644 index 000000000..b4b4594e5 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledBlueprints.php @@ -0,0 +1,44 @@ +object = new Blueprints($data); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + $this->object->embed($name, $file->content(), '/'); + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledConfig.php b/system/src/Grav/Common/Config/CompiledConfig.php new file mode 100644 index 000000000..70593df33 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledConfig.php @@ -0,0 +1,88 @@ +callable = $blueprints; + + return $this; + } + + /** + * @param bool $withDefaults + * @return mixed + */ + public function load($withDefaults = false) + { + $this->withDefaults = $withDefaults; + + return parent::load(); + } + + /** + * Create configuration object. + * + * @param array $data + */ + protected function createObject(array $data = []) + { + if ($this->withDefaults && empty($data) && is_callable($this->callable)) { + $blueprints = $this->callable; + $data = $blueprints()->getDefaults(); + } + + $this->object = new Config($data, $this->callable); + $this->object->checksum($this->checksum()); + $this->object->modified($this->modified()); + + if (method_exists($this->object, 'prepare')) { + $this->object->prepare(); + } + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + $this->object->join($name, $file->content(), '/'); + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledLanguages.php b/system/src/Grav/Common/Config/CompiledLanguages.php new file mode 100644 index 000000000..29c1c81ef --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledLanguages.php @@ -0,0 +1,48 @@ +object = new Languages($data); + $this->object->checksum($this->checksum()); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + if (preg_match('|languages\.yaml$|', $filename)) { + $this->object->mergeRecursive($file->content()); + } else { + $this->object->join($name, $file->content(), '/'); + } + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index ed13dbcbd..2fd3cc66d 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -1,12 +1,8 @@ [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['system'], - ] - ], - 'user' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user'], - ] - ], - 'asset' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['assets'], - ] - ], - 'blueprints' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://blueprints', 'system/blueprints'], - ] - ], - 'config' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://config', 'system/config'], - ] - ], - 'plugins' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://plugins'], - ] - ], - 'plugin' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://plugins'], - ] - ], - 'themes' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://themes'], - ] - ], - 'languages' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://languages', 'system/languages'], - ] - ], - 'cache' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['cache'], - 'images' => ['images'] - ] - ], - 'log' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['logs'] - ] - ], - 'backup' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['backup'] - ] - ] - ]; - - protected $setup = []; - - protected $blueprintFiles = []; - protected $configFiles = []; - protected $languageFiles = []; protected $checksum; - protected $timestamp; - - protected $configLookup; - protected $blueprintLookup; - protected $pluginLookup; - protected $languagesLookup; - - protected $finder; - protected $environment; - protected $messages = []; - - protected $languages; - - public function __construct(array $setup = array(), Grav $grav = null, $environment = null) - { - $this->grav = $grav ?: Grav::instance(); - $this->finder = new ConfigFinder; - $this->environment = $environment ?: 'localhost'; - $this->messages[] = 'Environment Name: ' . $this->environment; - - // Make sure that - if (!isset($setup['streams']['schemes'])) { - $setup['streams']['schemes'] = []; - } - $setup['streams']['schemes'] += $this->streams; - - $setup = $this->autoDetectEnvironmentConfig($setup); - - $this->setup = $setup; - parent::__construct($setup); - - $this->check(); - } public function key() { return $this->checksum(); } + public function checksum($checksum = null) + { + if ($checksum !== null) { + $this->checksum = $checksum; + } + + return $this->checksum; + } + + public function modified($modified = null) + { + if ($modified !== null) { + $this->modified = $modified; + } + + return $this->modified; + } + public function reload() { + throw new \Exception('TODO'); $this->items = $this->setup; - $this->check(); $this->init(); $this->debug(); return $this; } - protected function check() - { - $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null; - if (!is_array($streams)) { - throw new \InvalidArgumentException('Configuration is missing streams.schemes!'); - } - $diff = array_keys(array_diff_key($this->streams, $streams)); - if ($diff) { - throw new \InvalidArgumentException( - sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff)) - ); - } - } - public function debug() { - foreach ($this->messages as $message) { - $this->grav['debugger']->addMessage($message); + $debugger = Grav::instance()['debugger']; + $debugger->addMessage('Environment Name: ' . $this->environment); + if ($this->modified()) { + $debugger->addMessage('Configuration reloaded and cached.'); } - $this->messages = []; } public function init() { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - $this->configLookup = $locator->findResources('config://'); - $this->blueprintLookup = $locator->findResources('blueprints://config'); - $this->pluginLookup = $locator->findResources('plugins://'); - - - $this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master'); - $this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master'); - - // process languages if supported - if ($this->get('system.languages.translations', true)) { - $this->languagesLookup = $locator->findResources('languages://'); - $this->loadCompiledLanguages($this->languagesLookup, $this->pluginLookup, 'master'); - } - - $this->initializeLocator($locator); - } - - public function checksum() - { - if (empty($this->checksum)) { - $checkBlueprints = $this->get('system.cache.check.blueprints', false); - $checkLanguages = $this->get('system.cache.check.languages', false); - $checkConfig = $this->get('system.cache.check.config', true); - $checkSystem = $this->get('system.cache.check.system', true); - - if (!$checkBlueprints && !$checkLanguages && !$checkConfig && !$checkSystem) { - $this->messages[] = 'Skip configuration timestamp check.'; - return false; - } - - // Generate checksum according to the configuration settings. - if (!$checkConfig) { - // Just check changes in system.yaml files and ignore all the other files. - $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : []; + $setup = Grav::instance()['setup']->toArray(); + foreach ($setup as $key => $value) { + if ($key === 'streams' || !is_array($value)) { + // Optimized as streams and simple values are fully defined in setup. + $this->items[$key] = $value; } else { - // Check changes in all configuration files. - $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup); - } - - if ($checkBlueprints) { - $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup); - } else { - $cb = []; - } - - if ($checkLanguages) { - $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup); - } else { - $cl = []; - } - - $this->checksum = md5(json_encode([$cc, $cb, $cl])); - } - - return $this->checksum; - } - - protected function autoDetectEnvironmentConfig($items) - { - $environment = $this->environment; - $env_stream = 'user://'.$environment.'/config'; - - if (file_exists(USER_DIR.$environment.'/config')) { - array_unshift($items['streams']['schemes']['config']['prefixes'][''], $env_stream); - } - - return $items; - } - - protected function loadCompiledBlueprints($blueprints, $plugins, $filename = null) - { - $checksum = md5(json_encode($blueprints)); - $filename = $filename - ? CACHE_DIR . 'compiled/blueprints/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/blueprints/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $blueprintFiles = $this->finder->locateBlueprintFiles($blueprints, $plugins); - $checksum .= ':'.md5(json_encode($blueprintFiles)); - $class = get_class($this); - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load blueprints. - $this->blueprints = new Blueprints; - foreach ($blueprintFiles as $files) { - $this->loadBlueprintFiles($files); - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $blueprintFiles, - 'data' => $this->blueprints->toArray() - ]; - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled blueprints.'; - $file->save($cache); - $file->unlock(); - } - } else { - $this->blueprints = new Blueprints($cache['data']); - } - } - - protected function loadCompiledConfig($configs, $plugins, $filename = null) - { - $checksum = md5(json_encode($configs)); - $filename = $filename - ? CACHE_DIR . 'compiled/config/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/config/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $class = get_class($this); - $checksum = $this->checksum(); - - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['@class'] != $class - ) { - $this->messages[] = 'No cached configuration, compiling new configuration..'; - } else if ($cache['checksum'] !== $checksum) { - $this->messages[] = 'Configuration checksum mismatch, reloading configuration..'; - } else { - $this->messages[] = 'Configuration checksum matches, using cached version.'; - - $this->items = $cache['data']; - return; - } - - $configFiles = $this->finder->locateConfigFiles($configs, $plugins); - - // Attempt to lock the file for writing. - $file->lock(false); - - // Load configuration. - foreach ($configFiles as $files) { - $this->loadConfigFiles($files); - } - $cache = [ - '@class' => $class, - 'timestamp' => time(), - 'checksum' => $checksum, - 'data' => $this->toArray() - ]; - - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled configuration.'; - $file->save($cache); - $file->unlock(); - } - - $this->items = $cache['data']; - } - - /** - * @param $languages - * @param $plugins - * @param null $filename - */ - protected function loadCompiledLanguages($languages, $plugins, $filename = null) - { - $checksum = md5(json_encode($languages)); - $filename = $filename - ? CACHE_DIR . 'compiled/languages/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/languages/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $languageFiles = $this->finder->locateLanguageFiles($languages, $plugins); - $checksum .= ':' . md5(json_encode($languageFiles)); - $class = get_class($this); - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load languages. - $this->languages = new Languages; - $pluginPaths = str_ireplace(GRAV_ROOT . '/', '', array_reverse($plugins)); - foreach ($pluginPaths as $path) { - if (isset($languageFiles[$path])) { - foreach ((array) $languageFiles[$path] as $plugin => $item) { - $lang_file = CompiledYamlFile::instance($item['file']); - $content = $lang_file->content(); - $this->languages->mergeRecursive($content); - } - unset($languageFiles[$path]); - } - } - - foreach ($languageFiles as $location) { - foreach ($location as $lang => $item) { - $lang_file = CompiledYamlFile::instance($item['file']); - $content = $lang_file->content(); - $this->languages->join($lang, $content, '/'); - } - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $languageFiles, - 'data' => $this->languages->toArray() - ]; - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled languages.'; - $file->save($cache); - $file->unlock(); - } - } else { - $this->languages = new Languages($cache['data']); - } - } - - /** - * Load blueprints. - * - * @param array $files - */ - public function loadBlueprintFiles(array $files) - { - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->blueprints->embed($name, $file->content(), '/'); - } - } - - /** - * Load configuration. - * - * @param array $files - */ - public function loadConfigFiles(array $files) - { - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->join($name, $file->content(), '/'); - } - } - - /** - * Initialize resource locator by using the configuration. - * - * @param UniformResourceLocator $locator - */ - public function initializeLocator(UniformResourceLocator $locator) - { - $locator->reset(); - - $schemes = (array) $this->get('streams.schemes', []); - - foreach ($schemes as $scheme => $config) { - if (isset($config['paths'])) { - $locator->addPath($scheme, '', $config['paths']); - } - if (isset($config['prefixes'])) { - foreach ($config['prefixes'] as $prefix => $paths) { - $locator->addPath($scheme, $prefix, $paths); - } + $this->joinDefaults($key, $value); } } } /** - * Get available streams and their types from the configuration. - * - * @return array + * @return mixed + * @deprecated */ - public function getStreams() - { - $schemes = []; - foreach ((array) $this->get('streams.schemes') as $scheme => $config) { - $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; - if ($type[0] != '\\') { - $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; - } - - $schemes[$scheme] = $type; - } - - return $schemes; - } - public function getLanguages() { - return $this->languages; + return Grav::instance()['languages']; } } diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php new file mode 100644 index 000000000..887575a95 --- /dev/null +++ b/system/src/Grav/Common/Config/ConfigFileFinder.php @@ -0,0 +1,258 @@ +base = $base ? "{$base}/" : ''; + + return $this; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function locateFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list += $this->detectRecursive($folder, $pattern, $levels); + } + return $list; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function getFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + $files = $this->detectRecursive($folder, $pattern, $levels); + + $list += $files[trim($path, '/')]; + } + return $list; + } + + /** + * Return all paths for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function listFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels)); + } + return $list; + } + + /** + * Find filename from a list of folders. + * + * Note: Only finds the last override. + * + * @param string $filename + * @param array $folders + * @return array + */ + public function locateFileInFolder($filename, array $folders) + { + $list = []; + foreach ($folders as $folder) { + $list += $this->detectInFolder($folder, $filename); + } + return $list; + } + + /** + * Find filename from a list of folders. + * + * @param array $folders + * @param string $filename + * @return array + */ + public function locateInFolders(array $folders, $filename = null) + { + $list = []; + foreach ($folders as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + $list[$path] = $this->detectInFolder($folder, $filename); + } + return $list; + } + + /** + * Return all existing locations for a single file with a timestamp. + * + * @param array $paths Filesystem paths to look up from. + * @param string $name Configuration file to be located. + * @param string $ext File extension (optional, defaults to .yaml). + * @return array + */ + public function locateFile(array $paths, $name, $ext = '.yaml') + { + $filename = preg_replace('|[.\/]+|', '/', $name) . $ext; + + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_file("{$folder}/{$filename}")) { + $modified = filemtime("{$folder}/{$filename}"); + } else { + $modified = 0; + } + $basename = $this->base . $name; + $list[$path] = [$basename => ['file' => "{$path}/{$filename}", 'modified' => $modified]]; + } + + return $list; + } + + /** + * Detects all directories with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectRecursive($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { + return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return [$path => $list]; + } + + /** + * Detects all directories with the lookup file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $lookup Filename to be located (defaults to directory name). + * @return array + * @internal + */ + protected function detectInFolder($folder, $lookup = null) + { + $folder = rtrim($folder, '/'); + $path = trim(Folder::getRelativePath($folder), '/'); + $base = $path === $folder ? '' : ($path ? substr($folder, 0, -strlen($path)) : $folder . '/'); + + $list = []; + + if (is_dir($folder)) { + $iterator = new \DirectoryIterator($folder); + + /** @var \DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + $name = $directory->getBasename(); + $find = ($lookup ?: $name) . '.yaml'; + $filename = "{$path}/{$name}/{$find}"; + + if (file_exists($base . $filename)) { + $basename = $this->base . $name; + $list[$basename] = ['file' => $filename, 'modified' => filemtime($base . $filename)]; + } + } + } + + return $list; + } + + /** + * Detects all plugins with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectAll($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { + return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Config/ConfigFinder.php b/system/src/Grav/Common/Config/ConfigFinder.php deleted file mode 100644 index 46069410d..000000000 --- a/system/src/Grav/Common/Config/ConfigFinder.php +++ /dev/null @@ -1,186 +0,0 @@ -detectInFolder($folder, 'blueprints'); - } - foreach (array_reverse($blueprints) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - /** - * Get all locations for configuration files (including plugins). - * - * @param array $configs - * @param array $plugins - * @return array - */ - public function locateConfigFiles(array $configs, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectInFolder($folder); - } - foreach (array_reverse($configs) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - public function locateLanguageFiles(array $languages, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectLanguagesInFolder($folder, 'languages'); - } - foreach (array_reverse($languages) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - /** - * Get all locations for a single configuration file. - * - * @param array $folders Locations to look up from. - * @param string $name Filename to be located. - * @return array - */ - public function locateConfigFile(array $folders, $name) - { - $filename = "{$name}.yaml"; - - $list = []; - foreach ($folders as $folder) { - $path = trim(Folder::getRelativePath($folder), '/'); - - if (is_file("{$folder}/{$filename}")) { - $modified = filemtime("{$folder}/{$filename}"); - } else { - $modified = 0; - } - $list[$path] = [$name => ['file' => "{$path}/{$filename}", 'modified' => $modified]]; - } - - return $list; - } - - /** - * Detects all plugins with a configuration file and returns them with last modification time. - * - * @param string $folder Location to look up from. - * @param string $lookup Filename to be located. - * @return array - * @internal - */ - protected function detectInFolder($folder, $lookup = null) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - $list = []; - - if (is_dir($folder)) { - $iterator = new \FilesystemIterator($folder); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir()) { - continue; - } - - $name = $directory->getBasename(); - $find = ($lookup ?: $name) . '.yaml'; - $filename = "{$path}/{$name}/$find"; - - if (file_exists($filename)) { - $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - return [$path => $list]; - } - - protected function detectLanguagesInFolder($folder, $lookup = null) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - $list = []; - - if (is_dir($folder)) { - $iterator = new \FilesystemIterator($folder); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir()) { - continue; - } - - $name = $directory->getBasename(); - $find = ($lookup ?: $name) . '.yaml'; - $filename = "{$path}/{$name}/$find"; - - if (file_exists($filename)) { - $list[$name] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - return [$path => $list]; - } - - /** - * Detects all plugins with a configuration file and returns them with last modification time. - * - * @param string $folder Location to look up from. - * @return array - * @internal - */ - protected function detectRecursive($folder) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - if (is_dir($folder)) { - // Find all system and user configuration files. - $options = [ - 'compare' => 'Filename', - 'pattern' => '|\.yaml$|', - 'filters' => [ - 'key' => '|\.yaml$|', - 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { - return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; - } - ], - 'key' => 'SubPathname' - ]; - - $list = Folder::all($folder, $options); - } else { - $list = []; - } - - return [$path => $list]; - } -} diff --git a/system/src/Grav/Common/Config/Languages.php b/system/src/Grav/Common/Config/Languages.php index c0141292e..1ea8b3c87 100644 --- a/system/src/Grav/Common/Config/Languages.php +++ b/system/src/Grav/Common/Config/Languages.php @@ -12,6 +12,15 @@ use Grav\Common\Data\Data; class Languages extends Data { + public function checksum($checksum = null) + { + if ($checksum !== null) { + $this->checksum = $checksum; + } + + return $this->checksum; + } + public function reformat() { if (isset($this->items['plugins'])) { diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php new file mode 100644 index 000000000..ef89df015 --- /dev/null +++ b/system/src/Grav/Common/Config/Setup.php @@ -0,0 +1,232 @@ + [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['system'], + ] + ], + 'user' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user'], + ] + ], + 'environment' => [ + 'type' => 'ReadOnlyStream' + // If not defined, environment will be set up in the constructor. + ], + 'asset' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['assets'], + ] + ], + 'blueprints' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://blueprints', 'user://blueprints', 'system/blueprints'], + ] + ], + 'config' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://config', 'user://config', 'system/config'], + ] + ], + 'plugins' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'plugin' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'themes' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://themes'], + ] + ], + 'languages' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://languages', 'user://languages', 'system/languages'], + ] + ], + 'cache' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['cache'], + 'images' => ['images'] + ] + ], + 'log' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['logs'] + ] + ], + 'backup' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['backup'] + ] + ], + 'image' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://images', 'system://images'] + ] + ], + 'page' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://pages'] + ] + ], + 'account' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://accounts'] + ] + ], + ]; + + public function __construct($environment = 'localhost') + { + // Pre-load setup.php which contains our initial configuration. + // Configuration may contain dynamic parts, which is why we need to always load it. + $file = GRAV_ROOT . '/setup.php'; + $setup = is_file($file) ? (array) include $file : []; + + // Add default streams defined in beginning of the class. + if (!isset($setup['streams']['schemes'])) { + $setup['streams']['schemes'] = []; + } + $setup['streams']['schemes'] += $this->streams; + + // Initialize class. + parent::__construct($setup); + + // Set up environment. + $this->def('environment', $environment); + $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->environment}"]]); + } + + /** + * @return $this + */ + public function init() + { + $locator = new UniformResourceLocator(GRAV_ROOT); + $files = []; + + $guard = 5; + do { + $check = $files; + $this->initializeLocator($locator); + $files = $locator->findResources('config://streams.yaml'); + + if ($check === $files) { + break; + } + + // Update streams. + foreach ($files as $path) { + $file = CompiledYamlFile::instance($path); + $content = $file->content(); + if (!empty($content['schemes'])) { + $this->items['streams']['schemes'] = $content['schemes'] + $this->items['streams']['schemes']; + } + } + } while (--$guard); + + if (!$guard) { + throw new \RuntimeException('Setup: Configuration reload loop detected!'); + } + + // Make sure we have valid setup. + $this->check(); + + return $this; + } + + /** + * Initialize resource locator by using the configuration. + * + * @param UniformResourceLocator $locator + */ + public function initializeLocator(UniformResourceLocator $locator) + { + $locator->reset(); + + $schemes = (array) $this->get('streams.schemes', []); + + foreach ($schemes as $scheme => $config) { + if (isset($config['paths'])) { + $locator->addPath($scheme, '', $config['paths']); + } + if (isset($config['prefixes'])) { + foreach ($config['prefixes'] as $prefix => $paths) { + $locator->addPath($scheme, $prefix, $paths); + } + } + } + } + + /** + * Get available streams and their types from the configuration. + * + * @return array + */ + public function getStreams() + { + $schemes = []; + foreach ((array) $this->get('streams.schemes') as $scheme => $config) { + $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; + if ($type[0] != '\\') { + $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; + } + + $schemes[$scheme] = $type; + } + + return $schemes; + } + + /** + * @throws \InvalidArgumentException + */ + protected function check() + { + $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null; + if (!is_array($streams)) { + throw new \InvalidArgumentException('Configuration is missing streams.schemes!'); + } + $diff = array_keys(array_diff_key($this->streams, $streams)); + if ($diff) { + throw new \InvalidArgumentException( + sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff)) + ); + } + } +} diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index 28a6ac4d9..febdcd6a7 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -1,10 +1,15 @@ setup = is_file($file) ? (array) include $file : []; - $this->environment = isset($this->setup['environment']) ? $this->setup['environment'] : null; - - $container['blueprints'] = function ($c) use ($self) { - return $self->loadMasterBlueprints($c); + $container['setup'] = function ($c) { + return static::setup($c)->init(); }; - $container['config'] = function ($c) use ($self) { - return $self->loadMasterConfig($c); + $container['blueprints'] = function ($c) { + return static::blueprints($c); + }; + + $container['config'] = function ($c) { + return static::load($c); + }; + + $container['languages'] = function ($c) { + return static::languages($c); }; } - public function loadMasterConfig(Container $container) + public static function setup(Container $container) { - $environment = $this->getEnvironment($container); - - $config = new Config($this->setup, $container, $environment); - - return $config; + return new Setup($container['uri']->environment()); } - public function loadMasterBlueprints(Container $container) + public static function blueprints(Container $container) { - $environment = $this->getEnvironment($container); - $file = CACHE_DIR . 'compiled/blueprints/master-'.$environment.'.php'; - $data = is_file($file) ? (array) include $file : []; + /** Setup $setup */ + $setup = $container['setup']; - return new Blueprints($data, $container); + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/blueprints', true, true); + + $files = []; + $paths = $locator->findResources('blueprints://config'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints'); + + $blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT); + + return $blueprints->name("master-{$setup->environment}")->load(); } - public function getEnvironment(Container $container) + public static function load(Container $container) { - if (!isset($this->environment)) { - $this->environment = $container['uri']->environment(); + /** Setup $setup */ + $setup = $container['setup']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/config', true, true); + + $files = []; + $paths = $locator->findResources('config://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); + + $config = new CompiledConfig($cache, $files, GRAV_ROOT); + $config->setBlueprints(function() use ($container) { + return $container['blueprints']; + }); + + return $config->name("master-{$setup->environment}")->load(); + } + + public static function languages(Container $container) + { + /** Setup $setup */ + $setup = $container['setup']; + + /** @var Config $config */ + $config = $container['config']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/languages', true, true); + $files = []; + + // Process languages only if enabled in configuration. + if ($config->get('system.languages.translations', true)) { + $paths = $locator->findResources('languages://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages'); } - return $this->environment; + $languages = new CompiledLanguages($cache, $files, GRAV_ROOT); + + return $languages->name("master-{$setup->environment}")->load(); } } diff --git a/system/src/Grav/Common/Service/StreamsServiceProvider.php b/system/src/Grav/Common/Service/StreamsServiceProvider.php index d8f7dffbb..ec7f9cd85 100644 --- a/system/src/Grav/Common/Service/StreamsServiceProvider.php +++ b/system/src/Grav/Common/Service/StreamsServiceProvider.php @@ -1,7 +1,7 @@ initializeLocator($locator); + /** @var Setup $setup */ + $setup = $c['setup']; + $setup->initializeLocator($locator); return $locator; }; $container['streams'] = function($c) { - /** @var Config $config */ - $config = $c['config']; + /** @var Setup $setup */ + $setup = $c['setup']; /** @var UniformResourceLocator $locator */ $locator = $c['locator']; @@ -34,7 +34,7 @@ class StreamsServiceProvider implements ServiceProviderInterface Stream::setLocator($locator); ReadOnlyStream::setLocator($locator); - return new StreamBuilder($config->getStreams($c)); + return new StreamBuilder($setup->getStreams($c)); }; } }