метод
* @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;
}
}