метод * @var array */ protected $theme_tips_map = []; /** * Блоки меню * @var \bff\view\Menu[] */ protected $theme_menus = []; /** * Страницы с настройками * @var array */ protected $theme_pages = []; /** * Инициализация * @return void */ public function init() { if (! $this->isInited()) { $this->theme_inited = true; } else { return; } $this->extension_type = EXTENSION_TYPE_THEME; $this->module_name = $name = $this->getName(); $this->module_title = $this->getTitle(); $this->module_dir = $this->extension_path; if ($this->isBaseTheme()) { $this->theme_version = BFF_VERSION; } if ($this->isAdminPanel()) { foreach (['install','uninstall','activate','deactivate'] as $action) { $this->app->hookAdd('themes.' . $action . '.' . $name, function () use ($action) { if ($action !== 'deactivate') { $this->refreshStatic(in_array($action, ['install', 'activate'])); } return $this->$action(); }, 4); } } $this->app->hookAdd('themes.update.' . $name, function ($return, $context) { $this->refreshStatic(true); return $this->update($context); }, 4); $this->app->hookAdd('themes.start.' . $name, function ($obj, $testMode) { if (! $this->theme_started && ($this->isActive($testMode) || $obj->isParentTheme())) { $this->theme_started = true; if ($this->isDebug()) { $this->refreshStatic(true); } # Start: $this->onBeforeStart(); $this->start(); } }, 4); # Parent theme $parent = $this->getParentTheme(); if (! empty($parent) && is_string($parent) && ! $this->isBaseTheme()) { $parent = Dev::themeInstance($parent); if ($parent) { $this->setParentTheme($parent); Dev::themeInit($parent); } } # Theme settings if (! $this->isAddon()) { # Templates $this->logoSettings(); $this->faviconSettings(); $this->colorsSettings(); $this->fontsSettings(); # Settings foreach (['logo','favicon','colors','fonts','packages'] as $file) { $this->includeSettingsFile($file); } # Styles $this->cssEdit([ Theme::CSS_FILE_CUSTOM => ['path' => $this->path('/static/css/custom.css', false), 'save' => 'custom'], ]); } } /** * Инициализация темы выполнялась * @return bool */ public function isInited() { return $this->theme_inited; } /** * Logo settings * @param array|null $settings [ * 'position key' => [ * 'title' => 'position title', * 'tip' => 'position description', * 'default' => 'Default image URL', # e.g. /img/logo.png * ], * ... * ] * @param bool $extend extend default settings * @return bool */ protected function logoSettings(?array $settings = null, bool $extend = false): bool { if (is_null($settings)) { $this->configSettingsTemplateRegister('logo', [ 'template' => [ 'input' => 'image', 'image' => [ 'sizes' => [ 'view', ], ], 'tab' => '_logo', ], 'keys.overrideBy' => ['logo'], 'tab' => [ 'key' => '_logo', 'title' => _t('ext', 'Logo'), ], ], function ($settings) { foreach ($settings as $posKey => $posSettings) { $this->app->hookAdd('site.logo.url.' . $posKey, function ($url) use ($posKey, $posSettings) { if (! empty($posSettings['config.key']) && ! empty($this->extension_config[ $posSettings['config.key'] ]['configKey'])) { $data = config::get($this->extension_config[ $posSettings['config.key'] ]['configKey'], ''); $data = decodeFormData::getImageURL($data, 'o'); if (! empty($data) && is_string($data)) { return $data; } } return $url; }); } }); return true; } return $this->configSettingsTemplate('logo', $settings, $extend); } /** * Favicon images settings * @param array|null $settings [ * 'key' => [ * 'title' => 'название поля загрузки файла', * 'description' => 'примечание', * 'attr' => [ * # атрибуты мета-тега * 'rel' => 'icon', * ], * 'default' => 'URL по умолчанию', # например /favicon.ico * 'file' => [ * # допустимые параметры загружаемых файлов * 'extensionsAllowed' => 'png,svg,ico', # разрешенные расширения * 'maxSize' => '2097152', # максимально допустимый размер файла в байтах * ], * ], * ... * ] * @param bool $extend extend default settings * @return bool */ protected function faviconSettings(?array $settings = null, bool $extend = false): bool { if (is_null($settings)) { $this->configSettingsTemplateRegister('favicon', [ 'template' => [ 'input' => 'file', 'file' => [ 'extensionsAllowed' => 'png,svg,ico', 'maxSize' => 2097152, # 2Mb 'publicStore' => true, ], 'tab' => '_favicon', ], 'keys.overrideBy' => ['favicon'], 'tab' => [ 'key' => '_favicon', 'title' => _t('ext', 'Favicon'), ], ], function ($settings) { $iconsList = []; foreach ($settings as $iconKey => $iconSettings) { if (! empty($iconSettings['config.key']) && ! empty($this->extension_config[ $iconSettings['config.key'] ]['configKey'])) { $data = config::get($this->extension_config[ $iconSettings['config.key'] ]['configKey'], ''); $data = decodeFormData::getFileURL($data); if (! empty($data) && is_string($data)) { $iconSettings['attr']['href'] = $data; $iconsList[$iconKey] = $iconSettings['attr']; continue; } } if (! empty($iconSettings['default'])) { if (is_string($iconSettings['default'])) { $iconSettings['attr']['href'] = $iconSettings['default']; $iconsList[$iconKey] = $iconSettings['attr']; } elseif (is_array($iconSettings['default'])) { $iconsList[$iconKey] = $iconSettings['default']; } } } if (sizeof($iconsList) > 0) { $this->app->hookAdd('site.favicon.list', function ($list) use ($iconsList) { return $iconsList; }); } }); return true; } return $this->configSettingsTemplate('favicon', $settings, $extend); } /** * Theme colors settings * @param array|null $settings [ * 'color key' => [ * 'title' => 'colot title', * 'default' => 'Default color in format #000000', * ], * ... * ] * @param bool $extend extend default settings * @return mixed */ protected function colorsSettings(?array $settings = null, bool $extend = false): bool { if (is_null($settings)) { $this->configSettingsTemplateRegister('colors', [ 'template' => [ 'placeholder' => '#000000', 'input' => 'text', 'width' => 100, 'tab' => '_colors', 'toggle' => false, ], 'keys.overrideBy' => ['colors'], 'tab' => [ 'key' => '_colors', 'title' => _t('ext', 'Colors'), ], ], function ($settings) { $this->app->hooks()->cssExtra(true, function () use ($settings) { $colors = []; foreach ($settings as $key => $color) { $colors[$key] = $this->config($color['config.key']); } echo $this->includeSettingsFile('colors', $colors, false); }); }); return true; } foreach ($settings as $key => $color) { if (empty($color['toggle'])) { continue; } $k = $key . '_disabled'; if (isset($settings[$k])) { continue; } $settings[$k] = [ 'input' => 'checkbox', 'default' => false, 'description' => _t('@', 'Disabled'), 'tab' => '_colors', 'callable' => function ($form, $name) use ($key) { /** @var $form \bff\tpl\admin\Form */ $form->together($key); }, ]; } return $this->configSettingsTemplate('colors', $settings, $extend); } /** * Theme fonts settings * @param array|null $settings [ * 'font key' => [ * 'title' => 'font title', * 'options' => [], # available fonts list * 'default' => 'Default font from options list', * ], * ... * ] * @param bool $extend extend default settings * @return bool */ protected function fontsSettings(?array $settings = null, bool $extend = false): bool { if (is_null($settings)) { $this->configSettingsTemplateRegister('fonts', [ 'template' => [ 'input' => 'select', 'tab' => '_fonts', ], 'keys.overrideBy' => ['fonts'], 'tab' => [ 'key' => '_fonts', 'title' => _t('ext', 'Fonts'), ], ], function ($settings) { $this->app->hooks()->cssExtra(true, function () use ($settings) { $fonts = []; foreach ($settings as $key => $font) { $fonts[$key] = $this->config($font['config.key']); } echo $this->includeSettingsFile('fonts', $fonts, false); }); }); return true; } return $this->configSettingsTemplate('fonts', $settings, $extend); } /** * Add/get menu * @param string|string[] $id * @param string|string[]|null $title * @return \bff\view\Menu */ public function menu(string $id, $title = null) { # Create menu if (! array_key_exists($id, $this->theme_menus)) { $this->theme_menus[$id] = (new Menu())->setId($id)->setParent($this)->title($title ?? ''); } return $this->theme_menus[$id]; } /** * Menu list for editing * @return \bff\view\Menu[] */ public function menus() { return $this->theme_menus; } /** * Get all theme pages * @return array */ public function getPages() { if (! empty($this->theme_pages)) { return $this->theme_pages; } # Routes $pages = $this->router->getRoutesPages(); # Menus Site::menu(''); # register menus foreach ($this->theme_menus as $menu) { foreach ($menu->getPages() as $page) { $pages[] = $page; } } # Add & Sort foreach ($pages as $page) { if (is_object($page)) { $this->theme_pages[get_class($page) . spl_object_id($page)] = [ 'class' => get_class($page), 'instance' => $page, ]; } elseif (is_string($page)) { $this->theme_pages[$page] = [ 'class' => trim($page, '\\'), 'instance' => null, ]; } } ksort($this->theme_pages); return $this->theme_pages; } /** * Init page by class name * @param string $class * @return Page|null */ public function initPage($class) { if (empty($this->theme_pages)) { $this->getPages(); } if (array_key_exists($class, $this->theme_pages)) { $key = $class; } else { $key = false; foreach ($this->theme_pages as $k => $page) { if ($page['class'] === $class) { $key = $k; break; } } if ($key === false) { return null; } } $page = &$this->theme_pages[$key]; if (! is_null($page['instance'])) { return $page['instance']; } if (! class_exists($class)) { unset($this->theme_pages[$key]); return null; } $instance = $this->app->make($class); if (! ($instance instanceof Page)) { unset($this->theme_pages[$key]); return null; } if (isset($page['callback']) && is_callable($page['callback'])) { call_user_func($page['callback'], $instance); } return ($page['instance'] = $instance); } /** * Получить форму настроек для страницы по названию класса * @param string|Page $page page * @return Form|null */ public function getPageSettingsForm($page) { if (is_string($page)) { $page = $this->initPage($page); } if ($page instanceof Page) { return $this->initPageSettingsForm($page); } return null; } /** * Инициализация форм настроек * @return array */ public function initPagesSettingsForms() { $keys = []; $errors = []; foreach ($this->getPages() as $key => $data) { $page = $this->initPage($key); if ($page) { if ($page->getAliasFor()) { continue; } $form = $this->initPageSettingsForm($page); if (! $form) { continue; } $name = $form->name(); if (isset($keys[$name])) { $p = $keys[$name]['page']; $existing = '
Existing (key=' . $p->getKey() . ', class=' . get_class($p) . ')'; $new = '
New (key=' . $page->getKey() . ', class=' . get_class($page) . ')'; $errors[] = 'Page "' . $name . '" already exist.' . $existing . $new; } $keys[$name] = ['class' => $data['class'], 'page' => $page, 'form' => $form]; } } if (! empty($errors)) { $msg = 'Error initialization Settings Page
' . PHP_EOL . join('
' . PHP_EOL, $errors); $this->errors->set($msg); bff::log($msg); } return $keys; } /** * Инициализация формы настроек для страницы * @param Page $page * @return Form|null */ protected function initPageSettingsForm(Page $page) { $id = sizeof($this->extension_config_forms) + 1; $form = new Form($id, $this); $form->init(); $form->name($page->getKey()); $form->tabKey('page_' . $page->getKey()); $page->initSettingsForm($form); $this->extension_config_forms[$id] = $form; return $form; } /** * Сабмит формы настроек * @return array */ public function submitPagesSettingsForms() { do { $tab = $this->input->post('tab', TYPE_STR); if (! $tab) { break; } foreach ($this->extension_config_forms as $form) { if (! $form instanceof Form) { continue; } if ($form->tabKey() == $tab) { return $form->submit(); } } } while (false); return []; } /** * Include settings template file * @param string $file * @param array $data * @param bool $init * @return mixed */ public function includeSettingsFile(string $file, array $data = [], $init = true) { $data['init'] = $init; return $this->template($file, $data, ['throw' => false]); } /** * Подсказки для страницы * @param string $page ключ страницы или название метода темы * @param array $fields список требуемых полей * @param array $opts * @return array|mixed */ public function tips(string $page, array $fields, array $opts = []) { $method = 'tips_' . str_replace(['.','-'], '_', $page); if (method_exists($this, $method)) { return call_user_func([$this, $method], $fields, $opts); } $method = $this->theme_tips_map[$page] ?? $page; if (method_exists($this, $method)) { return call_user_func([$this, $method], $fields, $opts); } return []; } /** * Устанавливаем исходную тему * @param Theme|static|string $parent * @return bool * @throws Exception */ public function setParentTheme($parent): bool { if ($parent) { $this->theme_parent = $parent; } if (is_object($parent)) { $parent->addChildTheme($this); } return true; } /** * Возвращаем исходную тему * @return self|string */ public function getParentTheme() { return $this->theme_parent; } /** * Указана ли исходная тема * @return bool */ public function hasParentTheme(): bool { return ! empty($this->theme_parent) && is_object($this->theme_parent); } /** * Тема является наследником * @return bool */ public function isChildTheme(): bool { return $this->hasParentTheme(); } /** * Закрепляем связь с наследниками * @param Theme|static $child * @return bool */ public function addChildTheme(self $child): bool { if (! $this->theme_children) { $this->theme_children = new SplObjectStorage(); } if (! $this->theme_children->contains($child)) { $this->theme_children->attach($child); return true; } return false; } /** * Является ли данная тема исходной * @return bool */ public function isParentTheme(): bool { return ($this->theme_children && $this->theme_children->count() > 0); } /** * Является ли данная тема стандартной * @return bool */ public function isBaseTheme(): bool { return ($this->getName() === static::getBaseTheme()); } /** * Получаем название стандартной темы * @return string */ public static function getBaseTheme(): string { return bff::config('theme.base', 'platform', TYPE_STR); } /** * Получение настройки темы по ключу * Если настройка с таким ключем отсутствует обращаемся к методу исходной темы * {@inheritdoc} */ public function config($key, $default = '', $opts = []) { if (! $this->hasParentTheme()) { if ($this->hasAddons()) { $addonsReverse = array_reverse(iterator_to_array($this->extension_addons)); foreach ($addonsReverse as $addon) { if ($addon->isEnabled() && $addon->configExists($key)) { return $addon->config($key, $default, $opts); } } } if (parent::configExists($key)) { return parent::config($key, $default, $opts); } return $default; } $parent = $this->getParentTheme(); if (is_string($key)) { if (parent::configExists($key)) { return parent::config($key, $default, $opts); } else { return $parent->config($key, $default, $opts); } } elseif (is_array($key)) { $data = []; foreach ($key as $k => $v) { if (is_string($k)) { if ($this->configExists($k)) { $data[$k] = parent::config($k, $v, $opts); } else { $data[$k] = $parent->config($k, $v, $opts); } } elseif (is_string($v)) { if ($this->configExists($v)) { $data[$v] = parent::config($v, $default, $opts); } else { $data[$v] = $parent->config($v, $default, $opts); } } } return $data; } return $default; } /** * Проверка наличия настройки темы по ключу * Если настройка с таким ключем отсутствует проверяем в исходной теме * {@inheritdoc} */ public function configExists(string $key): bool { if (parent::configExists($key)) { return true; } elseif ($this->hasParentTheme()) { return $this->getParentTheme()->configExists($key); } elseif ($this->hasAddons()) { foreach ($this->extension_addons as $addon) { if ($addon->configExists($key)) { return true; } } } return false; } /** * Запуск темы (если была включена) * @return void */ protected function start() { } /** * Запуск темы выполнялся * @return bool */ public function isStarted() { return $this->theme_started; } /** * Установка темы * Метод вызываемый при инсталяции темы администратором * @return bool */ protected function install() { return true; } /** * Удаление темы * Метод вызываемый при удалении темы администратором * @return bool */ protected function uninstall() { return true; } /** * Установлена ли тема * @return bool */ public function isInstalled() { $list = bff::config('themes.installed.list', [], TYPE_ARRAY); return (is_array($list) && isset($list[$this->getName()])); } /** * Активация темы * Метод вызываемый при активации темы администратором * @return bool */ protected function activate() { return true; } /** * Деактивация темы * Метод вызываемый при дактивации темы администратором * @return bool */ protected function deactivate() { return true; } /** * Обновление темы * Метод вызываемый при обновлении темы * @param array $context [ * 'version_from' => 'версия до обновления (X.X.X)', * 'version_to' => 'версия обновления (X.X.X)', * 'date' => 'дата обновления (d.m.Y)' * ] * @return bool */ protected function update(array $context) { return true; } /** * Активирована ли тема * Алиас для isActive * @param bool|null $testMode включена ли тема в режиме тестирования * @return bool */ public function isEnabled($testMode = null) { return $this->isActive($testMode); } /** * Активирована ли тема * @param bool|null $testMode включена ли тема в режиме тестирования * @return bool */ public function isActive($testMode = null) { $testMode = $testMode ?? Dev::extensionsTestMode(); if ($testMode) { return $this->isTestmode(); } $theme = $this->app->theme(true, $testMode); return ( !empty($theme) && $this->getName() === $theme->getName() && ($this->isInstalled() || $this->isBaseTheme()) ); } /** * Внутреннее название темы * @return string */ public function getName() { return $this->theme_name; } /** * Видимое название темы * @return string */ public function getTitle() { if ( $this->theme_title === '?' || $this->theme_title === '{TITLE}' ) { return $this->getName(); } return $this->theme_title; } /** * Версия темы * @return string */ public function getVersion() { return $this->theme_version; } /** * Формирование URL файла * @param string $file название файла * @param string|null $version версия файла или null * @return string */ public function url(string $file, ?string $version = null) { return $this->app->url('/' . ltrim($file, '/ '), $version); } /** * Формирование абсолютного пути к файлу в директории темы * @param string $file относительный путь к файлу * @param array|bool $opts [mod, themed] * @return string */ public function path(string $file, $opts = []): string { $opts = $this->defaults((!is_array($opts) ? ['mod' => $opts] : $opts), [ 'mod' => true, 'themed' => true, ]); if ($opts['themed'] && $file) { $filePath = $this->fileThemed($file, false); if ($filePath !== false) { return $filePath . DS . ltrim($file, DS . ' '); } } return parent::path($file, $opts); } /** * Путь к файлу в директории темы * @param string $file относительный путь к файлу, начинается с "/" * @param bool $asUrl вернуть url * @param array $opts [public, &theme, &version] * @return string|bool путь/url к теме (в случае если запрашиваемый файл в ней был найден) */ public function fileThemed(string $file, bool $asUrl, array $opts = []) { # url if ($asUrl) { $theme = ''; $hotUrl = null; $opts['hotUrl'] = &$hotUrl; $opts['theme'] = &$theme; $opts['public'] = true; $path = $this->fileThemed($file, false, $opts); if ($path !== false) { return $opts['hotUrl'] ?? (SITEURL_STATIC . '/themes/' . $theme); } return false; } # path $public = $opts['public'] ?? false; if ($public) { $path = $this->pathPublic($file, ['mod' => false, 'themed' => false]); } else { $path = $this->path($file, ['mod' => false, 'themed' => false]); } # addons foreach ($this->extension_addons as $addon) { if ($addon->isTheme()) { $addonPath = $addon->fileThemed($file, $asUrl, $opts); if ($addonPath !== false) { return $addonPath; } } } # parent theme if (! file_exists($path)) { if ($this->isChildTheme()) { return $this->getParentTheme()->fileThemed($file, $asUrl, $opts); } return false; } $opts['theme'] = $this->getName(); if ($public) { $opts['version'] = $this->getStaticFileManifestVersion($file, [ 'hotUrl' => &$opts['hotUrl'], ]); return $path; } return rtrim($this->extension_path, '\/ '); } /** * Расписание запуска крон задач темы: * [ * 'название публичного метода темы' => ['period'=>'* * * * *'], * ... * ] * @return array */ public function cronSettings() { return []; } /** * Совпадает ли текущая активная тема с указанной (одной из списка указанных) * @param string|array $search название/id темы (нескольких тем) * @param bool $fallbackParent допустимо совпадение с parent-темой активной темы * @return bool текущая активная тема совпадает с указанной */ public static function compare($search, bool $fallbackParent = true): bool { if (empty($search)) { return false; } $active = bff::theme(); if ($active === false) { return false; } if (! is_array($search)) { $search = [$search]; } foreach ($search as $theme) { if (empty($theme)) { continue; } if ($active->getName() === $theme) { return true; } if ($active->getExtensionId() === $theme) { return true; } if ($fallbackParent && $active->isChildTheme()) { if ($active->getParentTheme()->getName() === $theme) { return true; } if ($active->getParentTheme()->getExtensionId() === $theme) { return true; } } } return false; } }