diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 609528428a..d774f5c191 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -67,10 +67,6 @@ public function boot() $this->bootMenuItemEvents(); $this->bootRichEditorEvents(); - - if ($this->app->runningInBackend()) { - $this->bootBackendLocalization(); - } } /** @@ -109,8 +105,18 @@ protected function registerTwigParser() ]; if ($useCache) { + $theme = Theme::getActiveTheme(); + $themeDir = $theme->getDirName(); + if ($parent = $theme->getConfig()['parent'] ?? false) { + $themeDir .= '-' . $parent; + } + $options['cache'] = new TwigCacheFilesystem( - storage_path().'/cms/twig', + storage_path(implode(DIRECTORY_SEPARATOR, [ + 'cms', + 'twig', + $themeDir, + ])) . DIRECTORY_SEPARATOR, $forceBytecode ? TwigCacheFilesystem::FORCE_BYTECODE_INVALIDATION : 0 ); } @@ -381,24 +387,6 @@ protected function registerBackendSettings() }); } - /** - * Boots localization from an active theme for backend items. - */ - protected function bootBackendLocalization() - { - $theme = Theme::getActiveTheme(); - - if (is_null($theme)) { - return; - } - - $langPath = $theme->getPath() . '/lang'; - - if (File::isDirectory($langPath)) { - Lang::addNamespace('themes.' . $theme->getId(), $langPath); - } - } - /** * Registers events for menu items. */ @@ -448,6 +436,7 @@ protected function registerHalcyonModels() { Event::listen('system.console.theme.sync.getAvailableModelClasses', function () { return [ + Classes\Theme::class, Classes\Meta::class, Classes\Page::class, Classes\Layout::class, diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 3b9aae08e5..c554b2f9ee 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -22,6 +22,11 @@ class AutoDatasource extends Datasource implements DatasourceInterface */ protected $datasources = []; + /** + * @var string The cache key to use for this datasource instance + */ + protected $cacheKey = 'halcyon-datastore-auto'; + /** * @var array Local cache of paths available in the datasources */ @@ -48,10 +53,14 @@ class AutoDatasource extends Datasource implements DatasourceInterface * @param array $datasources Array of datasources to utilize. Lower indexes = higher priority ['datasourceName' => $datasource] * @return void */ - public function __construct(array $datasources) + public function __construct(array $datasources, ?string $cacheKey = null) { $this->datasources = $datasources; + if ($cacheKey) { + $this->cacheKey = $cacheKey; + } + $this->activeDatasourceKey = array_keys($datasources)[0]; $this->populateCache(); @@ -59,6 +68,14 @@ public function __construct(array $datasources) $this->postProcessor = new Processor; } + /** + * Returns the in memory path cache map + */ + public function getPathCache(): array + { + return $this->pathCache; + } + /** * Populate the local cache of paths available in each datasource * @@ -69,6 +86,13 @@ public function populateCache($refresh = false) { $pathCache = []; foreach ($this->datasources as $datasource) { + // Allow AutoDatasource instances to handle their own internal caching + if ($datasource instanceof AutoDatasource) { + $datasource->populateCache($refresh); + $pathCache[] = array_merge(...array_reverse($datasource->getPathCache())); + continue; + } + // Remove any existing cache data if ($refresh && $this->allowCacheRefreshes) { Cache::forget($datasource->getPathsCacheKey()); @@ -309,7 +333,7 @@ protected function getValidPaths(string $dirName, array $options = []) */ protected function makeFilePath(string $dirName, string $fileName, string $extension) { - return $dirName . '/' . $fileName . '.' . $extension; + return ltrim($dirName . '/' . $fileName . '.' . $extension, '/'); } /** @@ -507,7 +531,7 @@ public function makeCacheKey($name = ''): string */ public function getPathsCacheKey(): string { - return 'halcyon-datastore-auto'; + return $this->cacheKey; } /** diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php index 961907f9c0..c2600a505a 100644 --- a/modules/cms/classes/CmsObject.php +++ b/modules/cms/classes/CmsObject.php @@ -124,7 +124,7 @@ public static function loadCached($theme, $fileName) * This method is used internally by the system. * @param \Cms\Classes\Theme $theme Specifies a parent theme. * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache. - * @return Collection Returns a collection of CMS objects. + * @return CmsObjectCollection Returns a collection of CMS objects. */ public static function listInTheme($theme, $skipCache = false) { diff --git a/modules/cms/classes/CodeParser.php b/modules/cms/classes/CodeParser.php index 66a340b4d0..5bb672133a 100644 --- a/modules/cms/classes/CodeParser.php +++ b/modules/cms/classes/CodeParser.php @@ -5,6 +5,7 @@ use Cache; use Config; use SystemException; +use Winter\Storm\Support\Str; /** * Parses the PHP code section of CMS objects. @@ -233,18 +234,22 @@ protected function storeCachedInfo($result) /** * Returns path to the cached parsed file - * @return string */ - protected function getCacheFilePath() + protected function getCacheFilePath(): string { - $hash = md5($this->filePath); - $result = storage_path().'/cms/cache/'; - $result .= substr($hash, 0, 2).'/'; - $result .= substr($hash, 2, 2).'/'; - $result .= basename($this->filePath); - $result .= '.php'; + $pathSegments = [ + storage_path('cms' . DIRECTORY_SEPARATOR . 'cache'), + trim( + Str::after( + pathinfo($this->filePath, PATHINFO_DIRNAME), + base_path() + ), + DIRECTORY_SEPARATOR + ), + basename($this->filePath) . '.php', + ]; - return $result; + return implode(DIRECTORY_SEPARATOR, $pathSegments); } /** diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index 95b3216a3b..5c18debe20 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -4,8 +4,10 @@ use Url; use App; use View; +use File; use Lang; use Flash; +use Cache; use Config; use Session; use Request; @@ -1353,22 +1355,57 @@ public function currentPageUrl($parameters = [], $routePersistence = true) * @param mixed $url Specifies the theme-relative URL. If null, the theme path is returned. * @return string */ - public function themeUrl($url = null) + public function themeUrl($url = null): string + { + return is_array($url) + ? $this->themeCombineAssets($url) + : $this->getTheme()->assetUrl($url); + } + + /** + * Generates a URL to the AssetCombiner for the provided array of assets + */ + protected function themeCombineAssets(array $url): string { $themeDir = $this->getTheme()->getDirName(); + $parentTheme = $this->getTheme()->getConfig()['parent'] ?? false; - if (is_array($url)) { - $_url = Url::to(CombineAssets::combine($url, themes_path().'/'.$themeDir)); - } - else { - $_url = Config::get('cms.themesPath', '/themes').'/'.$themeDir; - if ($url !== null) { - $_url .= '/'.$url; + $cacheKey = __METHOD__ . '.' . md5(json_encode($url)); + + if (!($assets = Cache::get($cacheKey))) { + $assets = []; + $sources = [ + themes_path($themeDir) + ]; + + if ($parentTheme) { + $sources[] = themes_path($parentTheme); } - $_url = Url::asset($_url); + + foreach ($url as $file) { + // Leave Combiner Aliases assets unmodified + if (str_starts_with($file, '@')) { + $assets[] = $file; + continue; + } + + foreach ($sources as $source) { + $asset = $source . DIRECTORY_SEPARATOR . $file; + if (File::exists($asset)) { + $assets[] = $asset; + break 2; + } + } + + // Skip combining missing assets and log an error + Log::error("$file could not be found in any of the theme's sources (" . implode(', ', $sources) . ','); + continue; + } + + Cache::put($cacheKey, $assets); } - return $_url; + return Url::to(CombineAssets::combine($assets)); } /** diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 9f3319dbad..0b82fc227f 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -8,6 +8,7 @@ use Cache; use Event; use Config; +use Schema; use Exception; use SystemException; use DirectoryIterator; @@ -26,7 +27,7 @@ * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges */ -class Theme +class Theme extends CmsObject { /** * @var string Specifies the theme directory name. @@ -48,6 +49,16 @@ class Theme */ protected static $editThemeCache = false; + /** + * @var array Allowable file extensions. + */ + protected $allowedExtensions = ['yaml']; + + /** + * @var string Default file extension. + */ + protected $defaultExtension = 'yaml'; + const ACTIVE_KEY = 'cms::theme.active'; const EDIT_KEY = 'cms::theme.edit'; @@ -55,21 +66,22 @@ class Theme * Loads the theme. * @return self */ - public static function load($dirName) + public static function load($dirName, $file = null): self { $theme = new static; $theme->setDirName($dirName); $theme->registerHalcyonDatasource(); + if (App::runningInBackend()) { + $theme->registerBackendLocalization(); + } return $theme; } /** * Returns the absolute theme path. - * @param string $dirName Optional theme directory. Defaults to $this->getDirName() - * @return string */ - public function getPath($dirName = null) + public function getPath(?string $dirName = null): string { if (!$dirName) { $dirName = $this->getDirName(); @@ -80,18 +92,16 @@ public function getPath($dirName = null) /** * Sets the theme directory name. - * @return void */ - public function setDirName($dirName) + public function setDirName(string $dirName): void { $this->dirName = $dirName; } /** * Returns the theme directory name. - * @return string */ - public function getDirName() + public function getDirName(): string { return $this->dirName; } @@ -99,19 +109,16 @@ public function getDirName() /** * Helper for {{ theme.id }} twig vars * Returns a unique string for this theme. - * @return string */ - public function getId() + public function getId(): string { return snake_case(str_replace('/', '-', $this->getDirName())); } /** * Determines if a theme with given directory name exists - * @param string $dirName The theme directory - * @return bool */ - public static function exists($dirName) + public static function exists(string $dirName): bool { $theme = static::load($dirName); $path = $theme->getPath(); @@ -122,10 +129,8 @@ public static function exists($dirName) /** * Returns a list of pages in the theme. * This method is used internally in the routing process and in the back-end UI. - * @param boolean $skipCache Indicates if the pages should be reloaded from the disk bypassing the cache. - * @return array Returns an array of \Cms\Classes\Page objects. */ - public function listPages($skipCache = false) + public function listPages(bool $skipCache = false): \Cms\Classes\CmsObjectCollection { return Page::listInTheme($this, $skipCache); } @@ -133,51 +138,21 @@ public function listPages($skipCache = false) /** * Returns true if this theme is the chosen active theme. */ - public function isActiveTheme() + public function isActiveTheme(): bool { $activeTheme = self::getActiveTheme(); - return $activeTheme && $activeTheme->getDirName() == $this->getDirName(); + return $activeTheme && $activeTheme->getDirName() === $this->getDirName(); } /** * Returns the active theme code. * By default the active theme is loaded from the cms.activeTheme parameter, * but this behavior can be overridden by the cms.theme.getActiveTheme event listener. - * @return string * If the theme doesn't exist, returns null. */ - public static function getActiveThemeCode() + public static function getActiveThemeCode(): string { - $activeTheme = Config::get('cms.activeTheme'); - $themes = static::all(); - $havingMoreThemes = count($themes) > 1; - $themeHasChanged = !empty($themes[0]) && $themes[0]->dirName !== $activeTheme; - $checkDatabase = $havingMoreThemes || $themeHasChanged; - - if ($checkDatabase && App::hasDatabase()) { - try { - try { - $expiresAt = now()->addMinutes(1440); - $dbResult = Cache::remember(self::ACTIVE_KEY, $expiresAt, function () { - return Parameter::applyKey(self::ACTIVE_KEY)->value('value'); - }); - } - catch (Exception $ex) { - // Cache failed - $dbResult = Parameter::applyKey(self::ACTIVE_KEY)->value('value'); - } - } - catch (Exception $ex) { - // Database failed - $dbResult = null; - } - - if ($dbResult !== null && static::exists($dbResult)) { - $activeTheme = $dbResult; - } - } - /** * @event cms.theme.getActiveTheme * Overrides the active theme code. @@ -185,30 +160,68 @@ public static function getActiveThemeCode() * If a value is returned from this halting event, it will be used as the active * theme code. Example usage: * - * Event::listen('cms.theme.getActiveTheme', function (string $activeTheme) { + * Event::listen('cms.theme.getActiveTheme', function () { * return 'mytheme'; * }); * */ - $apiResult = Event::fire('cms.theme.getActiveTheme', [$activeTheme], true); + $apiResult = Event::fire('cms.theme.getActiveTheme', [], true); if ($apiResult !== null) { - $activeTheme = $apiResult; + return $apiResult; + } + + // Load the active theme from the configuration + $activeTheme = $configuredTheme = Config::get('cms.activeTheme'); + + // Attempt to load the active theme from the cache before checking the database + try { + $cached = Cache::get(self::ACTIVE_KEY, null); + if ( + is_array($cached) + // Check if the configured theme has changed + && $cached['config'] === $configuredTheme + ) { + return $cached['active']; + } + } catch (Exception $ex) { + // Cache failed + } + + // Check the database + if (App::hasDatabase()) { + try { + $dbResult = Parameter::applyKey(self::ACTIVE_KEY)->value('value'); + } catch (Exception $ex) { + $dbResult = null; + } + + if ($dbResult !== null && static::exists($dbResult)) { + $activeTheme = $dbResult; + } } if (!strlen($activeTheme)) { throw new SystemException(Lang::get('cms::lang.theme.active.not_set')); } + // Cache the results + try { + Cache::forever(self::ACTIVE_KEY, [ + 'config' => $configuredTheme, + 'active' => $activeTheme, + ]); + } catch (Exception $ex) { + // Cache failed + } + return $activeTheme; } - /** * Returns the active theme object. - * @return \Cms\Classes\Theme Returns the loaded theme object. * If the theme doesn't exist, returns null. */ - public static function getActiveTheme() + public static function getActiveTheme(): self { if (self::$activeThemeCache !== false) { return self::$activeThemeCache; @@ -216,19 +229,15 @@ public static function getActiveTheme() $theme = static::load(static::getActiveThemeCode()); - if (!File::isDirectory($theme->getPath())) { - return self::$activeThemeCache = null; - } return self::$activeThemeCache = $theme; } /** - * Sets the active theme. + * Sets the active theme in the database. * The active theme code is stored in the database and overrides the configuration cms.activeTheme parameter. - * @param string $code Specifies the active theme code. */ - public static function setActiveTheme($code) + public static function setActiveTheme(string $code): void { self::resetCache(); @@ -255,15 +264,11 @@ public static function setActiveTheme($code) * but this behavior can be overridden by the cms.theme.getEditTheme event listeners. * If the edit theme is not defined in the configuration file, the active theme * is returned. - * @return string + * + * @throws SystemException if the edit theme cannot be determined */ - public static function getEditThemeCode() + public static function getEditThemeCode(): string { - $editTheme = Config::get('cms.editTheme'); - if (!$editTheme) { - $editTheme = static::getActiveThemeCode(); - } - /** * @event cms.theme.getEditTheme * Overrides the edit theme code. @@ -278,7 +283,12 @@ public static function getEditThemeCode() */ $apiResult = Event::fire('cms.theme.getEditTheme', [], true); if ($apiResult !== null) { - $editTheme = $apiResult; + return $apiResult; + } + + $editTheme = Config::get('cms.editTheme'); + if (!$editTheme) { + $editTheme = static::getActiveThemeCode(); } if (!strlen($editTheme)) { @@ -290,9 +300,8 @@ public static function getEditThemeCode() /** * Returns the edit theme. - * @return \Cms\Classes\Theme Returns the loaded theme object. */ - public static function getEditTheme() + public static function getEditTheme(): self { if (self::$editThemeCache !== false) { return self::$editThemeCache; @@ -300,18 +309,14 @@ public static function getEditTheme() $theme = static::load(static::getEditThemeCode()); - if (!File::isDirectory($theme->getPath())) { - return self::$editThemeCache = null; - } return self::$editThemeCache = $theme; } /** - * Returns a list of all themes. - * @return array Returns an array of the Theme objects. + * Returns an array of all themes. */ - public static function all() + public static function all(): array { $it = new DirectoryIterator(themes_path()); $it->rewind(); @@ -332,20 +337,27 @@ public static function all() /** * Reads the theme.yaml file and returns the theme configuration values. - * @return array Returns the parsed configuration file values. */ - public function getConfig() + public function getConfig(): array { if ($this->configCache !== null) { return $this->configCache; } - $path = $this->getPath().'/theme.yaml'; - if (!File::exists($path)) { + // Attempt to load the theme's config file from whatever datasources are available. + $sources = [ + 'filesystem' => new FileDatasource(themes_path($this->getDirName()), App::make('files')) + ]; + if (static::databaseLayerEnabled()) { + $sources['database'] = new DbDatasource($this->getDirName(), 'cms_theme_templates'); + } + $data = (new AutoDatasource($sources))->selectOne('', 'theme', 'yaml'); + + if (!$data) { return $this->configCache = []; } - $config = Yaml::parseFile($path); + $config = Yaml::parse($data['content']); /** * @event cms.theme.extendConfig @@ -371,9 +383,8 @@ public function getConfig() * Themes have a dedicated `form` option that provide form fields * for customization, this is an immutable accessor for that and * also an solid anchor point for extension. - * @return array */ - public function getFormConfig() + public function getFormConfig(): array { $config = $this->getConfigArray('form'); @@ -402,14 +413,53 @@ public function getFormConfig() return $config; } + /** + * Generates an asset URL for the provided path within the theme, will use the parent theme + * if the current theme does not actually have a directory on the filesystem (i.e. is virtual). + */ + public function assetUrl(?string $path): string + { + $expiresAt = now()->addMinutes(Config::get('cms.urlCacheTtl', 10)); + return Cache::remember('winter.cms.assetUrl.' . $path, $expiresAt, function () use ($path) { + $config = $this->getConfig(); + $themeDir = $this->getDirName(); + + // If the active theme does not have a directory, then just check the parent theme + if (!File::isDirectory(themes_path($this->getDirName())) && !empty($config['parent'])) { + $themeDir = $config['parent']; + } + + // Define a helper for constructing the URL + $urlPath = function ($themeDir, $path) { + $_url = Config::get('cms.themesPath', '/themes') . '/' . $themeDir; + + if ($path !== null) { + $_url .= '/' . $path; + } + + return $_url; + }; + + $url = $urlPath($themeDir, $path); + + // If the file cannot be found in the theme, generate a url for the parent theme + if (!File::exists(base_path($url)) && !empty($config['parent']) && $themeDir !== $config['parent']) { + $parentUrl = $urlPath($config['parent'], $path); + // If found in the parent, return it + if (File::exists(base_path($parentUrl))) { + return Url::asset($parentUrl); + } + } + + // Default to returning the current theme's url + return Url::asset($url); + }); + } + /** * Returns a value from the theme configuration file by its name. - * @param string $name Specifies the configuration parameter name. - * @param mixed $default Specifies the default value to return in case if the parameter - * doesn't exist in the configuration file. - * @return mixed Returns the parameter value or a default value */ - public function getConfigValue($name, $default = null) + public function getConfigValue(string $name, mixed $default = null): mixed { return array_get($this->getConfig(), $name, $default); } @@ -417,10 +467,8 @@ public function getConfigValue($name, $default = null) /** * Returns an array value from the theme configuration file by its name. * If the value is a string, it is treated as a YAML file and loaded. - * @param string $name Specifies the configuration parameter name. - * @return array */ - public function getConfigArray($name) + public function getConfigArray(string $name): array { $result = array_get($this->getConfig(), $name, []); @@ -446,11 +494,10 @@ public function getConfigArray($name) /** * Writes to the theme.yaml file with the supplied array values. - * @param array $values Data to write - * @param array $overwrite If true, undefined values are removed. - * @return void + * + * @throws ApplicationException if the theme.yaml file does not exist. */ - public function writeConfig($values = [], $overwrite = false) + public function writeConfig(array $values = [], bool $overwrite = false): void { if (!$overwrite) { $values = $values + (array) $this->getConfig(); @@ -458,7 +505,7 @@ public function writeConfig($values = [], $overwrite = false) $path = $this->getPath().'/theme.yaml'; if (!File::exists($path)) { - throw new ApplicationException('Path does not exist: '.$path); + throw new ApplicationException('Path does not exist: ' . $path); } $contents = Yaml::render($values); @@ -471,14 +518,13 @@ public function writeConfig($values = [], $overwrite = false) /** * Returns the theme preview image URL. * If the image file doesn't exist returns the placeholder image URL. - * @return string Returns the image URL. */ - public function getPreviewImageUrl() + public function getPreviewImageUrl(): string { $previewPath = $this->getConfigValue('previewImage', 'assets/images/theme-preview.png'); - if (File::exists($this->getPath().'/'.$previewPath)) { - return Url::asset('themes/'.$this->getDirName().'/'.$previewPath); + if (File::exists($this->getPath() . '/' . $previewPath)) { + return Url::asset('themes/' . $this->getDirName() . '/' . $previewPath); } return Url::asset('modules/cms/assets/images/default-theme-preview.png'); @@ -486,40 +532,39 @@ public function getPreviewImageUrl() /** * Resets any memory or cache involved with the active or edit theme. - * @return void */ - public static function resetCache() + public static function resetCache(bool $memoryOnly = false): void { self::$activeThemeCache = false; self::$editThemeCache = false; - Cache::forget(self::ACTIVE_KEY); - Cache::forget(self::EDIT_KEY); + // Sometimes it may be desired to only clear the local cache of the active / edit themes instead of the persistent cache + if (!$memoryOnly) { + Cache::forget(self::ACTIVE_KEY); + Cache::forget(self::EDIT_KEY); + } } /** * Returns true if this theme has form fields that supply customization data. - * @return bool */ - public function hasCustomData() + public function hasCustomData(): bool { - return $this->getConfigValue('form', false); + return (bool) $this->getConfigValue('form', false); } /** * Returns data specific to this theme - * @return Cms\Models\ThemeData */ - public function getCustomData() + public function getCustomData(): ThemeData { return ThemeData::forTheme($this); } /** * Remove data specific to this theme - * @return bool */ - public function removeCustomData() + public function removeCustomData(): bool { if ($this->hasCustomData()) { return $this->getCustomData()->delete(); @@ -528,49 +573,100 @@ public function removeCustomData() return true; } + /** + * Register the backend localizations provided by this theme and its ancestors. + */ + public function registerBackendLocalization(): void + { + $langPath = $this->getPath() . '/lang'; + + if (File::isDirectory($langPath)) { + Lang::addNamespace($this->getDirName(), $langPath); + } + + // Check the parent theme if present + $config = $this->getConfig(); + if (!empty($config['parent'])) { + $langPath = themes_path($config['parent'] . '/lang'); + if (File::isDirectory($langPath)) { + Lang::addNamespace('themes.' . $config['parent'], $langPath); + } + } + } + /** * Checks to see if the database layer has been enabled - * - * @return boolean */ - public static function databaseLayerEnabled() + public static function databaseLayerEnabled(): bool { $enableDbLayer = Config::get('cms.databaseTemplates', false); if (is_null($enableDbLayer)) { $enableDbLayer = !Config::get('app.debug', false); } - return $enableDbLayer && App::hasDatabase(); + $hasDb = Cache::rememberForever('cms.databaseTemplates.hasTables', function () { + return App::hasDatabase() && Schema::hasTable('cms_theme_templates'); + }); + + return $enableDbLayer && $hasDb; } /** * Ensures this theme is registered as a Halcyon datasource. - * @return void */ - public function registerHalcyonDatasource() + public function registerHalcyonDatasource(): void { $resolver = App::make('halcyon'); + if ($resolver->hasDatasource($this->dirName)) { + return; + } + + $sources = []; + if (static::databaseLayerEnabled()) { + $sources['database'] = new DbDatasource($this->dirName, 'cms_theme_templates'); + } - if (!$resolver->hasDatasource($this->dirName)) { + $sources['filesystem'] = new FileDatasource($this->getPath(), App::make('files')); + + $config = $this->getConfig(); + if (!empty($config['parent'])) { if (static::databaseLayerEnabled()) { - $datasource = new AutoDatasource([ - 'database' => new DbDatasource($this->dirName, 'cms_theme_templates'), - 'filesystem' => new FileDatasource($this->getPath(), App::make('files')), - ]); - } else { - $datasource = new FileDatasource($this->getPath(), App::make('files')); + $sources['parent-database'] = new DbDatasource($config['parent'], 'cms_theme_templates'); } - $resolver->addDatasource($this->dirName, $datasource); + $sources['parent-filesystem'] = new FileDatasource(themes_path($config['parent']), App::make('files')); } + + $datasource = count($sources) > 1 + ? new AutoDatasource($sources, 'halcyon-datasource-auto-' . $this->dirName) + : array_shift($sources); + + $resolver->addDatasource($this->dirName, $datasource); + + /** + * @event cms.theme.registerHalcyonDatasource + * Fires immediately after the theme's Datasource has been registered. + * + * Allows for extension of the theme Halcyon Datasource, example usage: + * + * use Cms\Classes\Theme; + * use Winter\Storm\Halcyon\Datasource\Resolver; + * + * Event::listen('cms.theme.registerHalcyonDatasource', function (Theme $theme, Resolver $resolver) { + * $resolver->addDatasource($theme->getDirName(), new AutoDatasource([ + * 'theme' => $theme->getDatasource(), + * 'example' => new ExampleDatasource(), + * ], 'example-autodatasource')); + * }); + * + */ + Event::fire('cms.theme.registerHalcyonDatasource', [$this, $resolver]); } /** * Get the theme's datasource - * - * @return DatasourceInterface */ - public function getDatasource() + public function getDatasource(): AutoDatasource { $resolver = App::make('halcyon'); return $resolver->datasource($this->getDirName()); @@ -578,8 +674,6 @@ public function getDatasource() /** * Implements the getter functionality. - * @param string $name - * @return void */ public function __get($name) { @@ -592,8 +686,6 @@ public function __get($name) /** * Determine if an attribute exists on the object. - * @param string $key - * @return void */ public function __isset($key) { diff --git a/modules/cms/tests/classes/CmsObjectTest.php b/modules/cms/tests/classes/CmsObjectTest.php index 9d3f3b4b99..61bacb711a 100644 --- a/modules/cms/tests/classes/CmsObjectTest.php +++ b/modules/cms/tests/classes/CmsObjectTest.php @@ -265,6 +265,29 @@ public function testRename() $this->assertEquals($testContents, file_get_contents($destFilePath)); } + /** + * @depends testRename + */ + public function testSaveSameName() + { + $theme = Theme::load('apitest'); + + $filePath = $theme->getPath() . '/testobjects/anotherobj.htm'; + $this->assertFileExists($filePath); + + $testContents = 'new content'; + $obj = TestCmsObject::load($theme, 'anotherobj.htm'); + + $obj->fill([ + 'fileName' => 'anotherobj', + 'content' => $testContents + ]); + $obj->save(); + + $this->assertFileExists($filePath); + $this->assertEquals($testContents, file_get_contents($filePath)); + } + /** * @depends testRename */ @@ -289,29 +312,6 @@ public function testRenameToExistingFile() $obj->save(); } - /** - * @depends testRename - */ - public function testSaveSameName() - { - $theme = Theme::load('apitest'); - - $filePath = $theme->getPath() . '/testobjects/anotherobj.htm'; - $this->assertFileExists($filePath); - - $testContents = 'new content'; - $obj = TestCmsObject::load($theme, 'anotherobj.htm'); - - $obj->fill([ - 'fileName' => 'anotherobj', - 'content' => $testContents - ]); - $obj->save(); - - $this->assertFileExists($filePath); - $this->assertEquals($testContents, file_get_contents($filePath)); - } - public function testSaveNewDir() { $theme = Theme::load('apitest'); diff --git a/modules/cms/tests/classes/ControllerTest.php b/modules/cms/tests/classes/ControllerTest.php index af34603a0a..e888f37462 100644 --- a/modules/cms/tests/classes/ControllerTest.php +++ b/modules/cms/tests/classes/ControllerTest.php @@ -164,6 +164,17 @@ public function testPartials() $this->assertEquals('
Hey PAGE PARTIAL Homer Simpson A partial
Hey PAGE PARTIAL Homer Simpson A child partial
Hey PAGE CONTENT A content
Hey PAGE CONTENT A child content
Hey PAGE CONTENT
Hey PAGE CONTENT