Files
ave-cms/system/App/Common/ModuleManager.php
T
2026-07-27 12:58:44 +03:00

1617 lines
58 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Common/ModuleManager.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Common;
use App\Common\Loader\Load;
use App\Content\Fields\FieldRegistry;
use App\Content\Blocks\ContentBlockRegistry;
use App\Content\Requests\RequestRendererRegistry;
use App\Content\Rubrics\FieldSetRegistry;
use App\Helpers\Hooks;
use App\Helpers\Dir;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
/**
* Загрузчик модулей.
*
* Модуль описывает себя декларативно в module.php и возвращает массив-дескриптор:
* code, name, version, lifecycle, config, settings, migrations, files,
* helpers, routes, permissions, navigation, hooks, view_globals, admin_extension.
* ModuleManager регистрирует всё это в общих реестрах ядра
* (Permission, Navigation, Hooks) и хранит глобальные переменные для шаблонов.
*
* Ядро не знает о конкретных модулях — знание живёт только в module.php.
*/
class ModuleManager
{
/** @var array code => ['code','name','version'] */
protected static $modules = [];
/** @var array code => полный module.php descriptor */
protected static $descriptors = [];
/** @var array key => value|callable */
protected static $viewGlobals = [];
/** @var string[] */
protected static $publicRoutes = [];
/** @var string[] */
protected static $publicPrefixes = [];
/** @var string[] */
protected static $errors = [];
/** Режим загрузки: 'admin' | 'public' | 'all'. */
protected static $mode = 'admin';
/** @var bool */
protected static $lifecycleTableEnsured = false;
/** @var array|null Состояния модулей, загруженные одним запросом в public runtime. */
protected static $publicLifecycleStates;
protected function __construct()
{
//
}
public static function mode()
{
return self::$mode;
}
/**
* Установить режим для последующей точечной загрузки через load().
* Нужен public runtime, где manifests лежат в app-папках legacy-модулей.
*/
public static function setMode($mode)
{
self::$mode = in_array($mode, array('admin', 'public', 'all', 'shared'), true) ? $mode : 'admin';
}
/** Загрузить модуль из файла-дескриптора (module.php). */
public static function load($file)
{
if (!is_file($file)) {
return null;
}
$descriptor = include $file;
if (!is_array($descriptor)) {
return null;
}
$descriptor['_base_path'] = dirname($file);
self::register($descriptor);
return $descriptor;
}
/**
* Автоматически загрузить все модули из директории.
*
* Ожидаемый формат:
* system/App/Marketplace/module.php
* system/App/SomeModule/module.php
*
* Для каждой директории с module.php регистрируется PSR-4 namespace:
* App\Marketplace => system/App/Marketplace
*
* @param string $baseDir Директория с модулями
* @param string $namespaceRoot Корневой namespace
* @return array ['loaded' => [...], 'errors' => [...]]
*/
public static function loadAll($baseDir, $mode = 'admin', $namespaceRoot = 'App')
{
self::$mode = in_array($mode, ['admin', 'public', 'all', 'shared'], true) ? $mode : 'admin';
$result = [
'loaded' => [],
'errors' => [],
];
$baseDir = rtrim((string) $baseDir, DS);
if (!is_dir($baseDir)) {
$result['errors'][] = $baseDir;
self::$errors[] = $baseDir;
return $result;
}
$compiledScope = self::compiledScope($baseDir, $namespaceRoot);
$compiled = $compiledScope !== '' ? CompiledModuleRegistry::load($compiledScope, array($baseDir)) : null;
if (is_array($compiled)) {
foreach ($compiled as $entry) {
$descriptor = is_array($entry) ? CompiledModuleRegistry::activate($entry) : null;
if (!$descriptor) {
$name = is_array($entry) && isset($entry['folder']) ? (string) $entry['folder'] : 'unknown';
$result['errors'][] = $name;
self::$errors[] = $name;
CompiledModuleRegistry::invalidate($compiledScope);
continue;
}
$result['loaded'][] = isset($descriptor['code']) ? (string) $descriptor['code'] : (string) $entry['folder'];
}
return $result;
}
$entries = scandir($baseDir);
if (!$entries) {
return $result;
}
$compiledEntries = array();
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || strpos($entry, '.') === 0) {
continue;
}
$dir = $baseDir . DS . $entry;
if (!is_dir($dir)) {
continue;
}
$moduleFile = $dir . DS . 'module.php';
if (!is_file($moduleFile)) {
continue;
}
Load::regNamespace(trim($namespaceRoot, '\\') . '\\' . $entry, $dir);
$descriptor = self::load($moduleFile);
if (!$descriptor) {
$result['errors'][] = $entry;
self::$errors[] = $entry;
continue;
}
$result['loaded'][] = isset($descriptor['code']) ? (string) $descriptor['code'] : $entry;
if ($compiledScope !== '') {
$compiledEntries[] = CompiledModuleRegistry::entry(
$entry,
$dir,
$moduleFile,
array(trim($namespaceRoot, '\\') . '\\' . $entry => $dir),
$descriptor
);
}
}
if ($compiledScope !== '') {
CompiledModuleRegistry::store($compiledScope, $compiledEntries, array($baseDir));
}
return $result;
}
protected static function compiledScope($baseDir, $namespaceRoot)
{
$expected = AdminLocation::path('modules');
$actual = rtrim(str_replace('\\', '/', (string) $baseDir), '/');
return $actual === $expected && trim((string) $namespaceRoot, '\\') === 'App\\Adminx'
? 'adminx'
: '';
}
/** Зарегистрировать модуль из массива-дескриптора. */
public static function register(array $descriptor)
{
if (empty($descriptor['code'])) {
return;
}
$code = (string) $descriptor['code'];
$basePath = isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '';
self::registerNamespaces($basePath, isset($descriptor['namespaces']) ? $descriptor['namespaces'] : null);
$lifecycle = self::normalizeLifecycle(isset($descriptor['lifecycle']) ? $descriptor['lifecycle'] : null);
$state = self::resolveLifecycleState($code, $descriptor, $lifecycle);
self::$descriptors[$code] = $descriptor;
self::$modules[$code] = [
'code' => $code,
'name' => isset($descriptor['name']) ? (string) $descriptor['name'] : $code,
'version' => isset($descriptor['version']) ? (string) $descriptor['version'] : '',
'base_path' => $basePath,
'description' => isset($descriptor['description']) ? (string) $descriptor['description'] : '',
'author' => isset($descriptor['author']) ? (string) $descriptor['author'] : '',
'lifecycle' => $lifecycle,
'managed' => !empty($lifecycle['managed']),
'installed' => $state['installed'],
'enabled' => $state['enabled'],
'status' => $state['status'],
'db_version' => $state['version'],
'recoverable' => !empty($state['recoverable']),
'operation' => isset($state['operation']) ? $state['operation'] : '',
'previous_status' => isset($state['previous_status']) ? $state['previous_status'] : '',
'last_error' => isset($state['last_error']) ? $state['last_error'] : '',
'admin_extension' => isset($descriptor['admin_extension']) && is_array($descriptor['admin_extension']) ? $descriptor['admin_extension'] : array(),
'dependencies' => self::normalizeDependencies(isset($descriptor['requires']) ? $descriptor['requires'] : null),
'permissions_count' => self::countPermissions(isset($descriptor['permissions']) ? $descriptor['permissions'] : null),
'navigation_count' => self::countItems(isset($descriptor['navigation']) ? $descriptor['navigation'] : null),
'hooks_count' => self::countItems(isset($descriptor['hooks']) ? $descriptor['hooks'] : null),
'field_types_count' => self::countItems(isset($descriptor['field_types']) ? $descriptor['field_types'] : null),
'field_sets_count' => self::countItems(isset($descriptor['field_sets']) ? $descriptor['field_sets'] : null),
'request_renderers_count' => self::countItems(isset($descriptor['request_renderers']) ? $descriptor['request_renderers'] : null),
'content_blocks_count' => self::countItems(isset($descriptor['content_blocks']) ? $descriptor['content_blocks'] : null),
'config_count' => self::countConfig(isset($descriptor['config']) ? $descriptor['config'] : null),
'settings_count' => self::countItems(isset($descriptor['settings']) ? $descriptor['settings'] : null),
'migrations_count' => self::countFiles(isset($descriptor['migrations']) ? $descriptor['migrations'] : null),
'helpers_count' => self::countFiles(isset($descriptor['helpers']) ? $descriptor['helpers'] : null),
'files_count' => self::countFiles(isset($descriptor['files']) ? $descriptor['files'] : null),
'routes_count' => self::countFiles(isset($descriptor['routes']) ? $descriptor['routes'] : null),
];
$mode = self::$mode;
$admin = (isset($descriptor['admin']) && is_array($descriptor['admin'])) ? $descriptor['admin'] : array();
$public = (isset($descriptor['public']) && is_array($descriptor['public'])) ? $descriptor['public'] : array();
//-- Управляемый модуль без записи lifecycle только обнаруживается.
//-- Его код, миграции и UI подключатся после явной установки.
if (!empty($lifecycle['managed']) && !$state['installed']) {
Hooks::action('module.registered', $code);
return;
}
//-- Shared (в любом режиме): config, files, helpers, services,
//-- view-globals, shared-хуки, публичный доступ к роутам (login-bypass).
self::registerConfig($basePath, isset($descriptor['config']) ? $descriptor['config'] : null);
self::registerSettings($code, isset($descriptor['settings']) ? $descriptor['settings'] : null);
self::registerFiles($basePath, isset($descriptor['files']) ? $descriptor['files'] : null);
self::registerFiles($basePath, isset($descriptor['helpers']) ? $descriptor['helpers'] : null);
self::registerFiles($basePath, isset($descriptor['services']) ? $descriptor['services'] : null);
self::registerViewGlobals(isset($descriptor['view_globals']) ? $descriptor['view_globals'] : null);
HookCatalog::registerMany(isset($descriptor['hook_definitions']) && is_array($descriptor['hook_definitions']) ? $descriptor['hook_definitions'] : array(), 'module:' . $code);
self::registerFieldTypes($code, isset($descriptor['field_types']) ? $descriptor['field_types'] : null, !empty($state['enabled']));
self::registerFieldSets($code, isset($descriptor['field_sets']) ? $descriptor['field_sets'] : null, !empty($state['enabled']));
self::registerRequestRenderers($code, isset($descriptor['request_renderers']) ? $descriptor['request_renderers'] : null, !empty($state['enabled']));
self::registerContentBlocks($code, isset($descriptor['content_blocks']) ? $descriptor['content_blocks'] : null, !empty($state['enabled']));
self::registerHooks(isset($descriptor['hooks']) ? $descriptor['hooks'] : null);
self::registerPublicAccess($descriptor);
//-- Реестр модулей (§16.10): disabled-модуль грузит shared (данные/схема),
//-- но НЕ admin/public (routes/menu/hooks). Core не выключается.
$disabled = !empty($lifecycle['managed']) ? !$state['enabled'] : self::isDisabled($code);
//-- Admin runtime: admin-секция или legacy top-level (обратная совместимость).
if (!$disabled && ($mode === 'admin' || $mode === 'all')) {
self::registerFiles($basePath, isset($admin['services']) ? $admin['services'] : null);
self::registerPermissions($code, isset($admin['permissions']) ? $admin['permissions'] : (isset($descriptor['permissions']) ? $descriptor['permissions'] : null));
self::registerNavigation(isset($admin['navigation']) ? $admin['navigation'] : (isset($descriptor['navigation']) ? $descriptor['navigation'] : null));
self::registerHooks(isset($admin['hooks']) ? $admin['hooks'] : null);
self::registerAdminAssets(isset($admin['assets']) ? $admin['assets'] : (isset($descriptor['assets']) ? $descriptor['assets'] : null));
self::registerRoutes(
$basePath,
isset($admin['routes']) ? $admin['routes'] : (isset($descriptor['routes']) ? $descriptor['routes'] : null),
self::adminRouteOptions($descriptor, $admin)
);
}
//-- Public runtime: только public-секция (не зависит от админки).
if (!$disabled && ($mode === 'public' || $mode === 'all')) {
self::registerFiles($basePath, isset($public['services']) ? $public['services'] : null);
self::registerRoutes($basePath, isset($public['routes']) ? $public['routes'] : null);
self::registerHooks(isset($public['hooks']) ? $public['hooks'] : null);
}
Hooks::action('module.registered', $code);
}
/** Зарегистрированные модули (метаданные). */
public static function all()
{
return array_values(self::$modules);
}
/** Полный descriptor зарегистрированного модуля. */
public static function descriptor($code)
{
return isset(self::$descriptors[$code]) ? self::$descriptors[$code] : null;
}
/** Метаданные одного зарегистрированного модуля. */
public static function get($code)
{
return self::moduleMeta((string) $code);
}
public static function isManaged($code)
{
$module = self::moduleMeta($code);
return $module && !empty($module['managed']);
}
/** Prepare a destructive lifecycle action and issue a server-side confirmation token. */
public static function preflight($code, $operation)
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (empty($meta['installed'])) {
throw new \RuntimeException('Модуль не установлен');
}
$operation = self::normalizePreflightOperation($operation);
$preview = self::lifecyclePreview($descriptor, $operation);
if ($preview === null) {
return array('required' => false, 'operation' => $operation, 'preview' => array());
}
$token = ModuleLifecyclePreflight::issue($code, $operation, Auth::id(), $preview);
return array(
'required' => true,
'operation' => $operation,
'token' => $token,
'expires_in' => ModuleLifecyclePreflight::TTL,
'preview' => $preview,
);
}
/** Установить обнаруженный управляемый модуль. */
public static function install($code)
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (isset($meta['status']) && $meta['status'] === 'dirty') {
throw new \RuntimeException('Предыдущая операция не завершена. Используйте восстановление модуля');
}
if (!empty($meta['installed'])) {
throw new \RuntimeException('Модуль уже установлен');
}
self::assertCoreSchemaReady();
self::assertDependencies($descriptor);
self::runLifecycleChange($descriptor, 'install', 'available');
self::writeLifecycleEvent($code, 'installed', 'Модуль установлен');
Hooks::action('module.installed', new LifecycleEvent('module', 'installed', $code, array(
'module' => self::refreshLifecycleMeta($code),
), null, array(), 'module_manager'));
return self::refreshLifecycleMeta($code);
}
/** Применить новые миграции и обновить версию файлов. */
public static function update($code)
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (empty($meta['installed'])) {
throw new \RuntimeException('Модуль не установлен');
}
$previousStatus = !empty($meta['enabled']) ? 'enabled' : 'disabled';
self::runLifecycleChange($descriptor, 'update', $previousStatus);
self::writeLifecycleEvent($code, 'updated', 'Модуль обновлен');
Hooks::action('module.updated', new LifecycleEvent('module', 'updated', $code, array(
'module' => self::refreshLifecycleMeta($code),
), null, array(), 'module_manager'));
return self::refreshLifecycleMeta($code);
}
/** Повторить оборванную установку или обновление по журналу состояния. */
public static function repair($code)
{
$descriptor = self::managedDescriptor($code);
$state = self::lifecycleState($code, true);
if (!$state || !isset($state['status']) || (string) $state['status'] !== 'dirty') {
throw new \RuntimeException('Модуль не требует восстановления');
}
$operation = isset($state['operation']) ? (string) $state['operation'] : '';
if (!in_array($operation, array('install', 'update', 'uninstall'), true)) {
throw new \RuntimeException('Неизвестна оборванная операция модуля');
}
$previousStatus = isset($state['previous_status']) ? (string) $state['previous_status'] : 'available';
if (!in_array($previousStatus, array('available', 'enabled', 'disabled'), true)) {
$previousStatus = $operation === 'install' ? 'available' : 'disabled';
}
if ($operation === 'uninstall') {
self::runUninstallChange($descriptor, $previousStatus);
} else {
self::assertDependencies($descriptor);
self::runLifecycleChange($descriptor, $operation, $previousStatus);
}
self::writeLifecycleEvent($code, 'repaired', 'Операция модуля восстановлена');
Hooks::action('module.repaired', new LifecycleEvent('module', 'repaired', $code, array(
'module' => self::refreshLifecycleMeta($code),
'operation' => $operation,
), null, array(), 'module_manager'));
return self::refreshLifecycleMeta($code);
}
/**
* Применить миграции встроенных разделов явной web-операцией.
* Управляемые пакеты обновляются только через install()/update().
*/
public static function updateCoreSchema()
{
$results = array();
foreach (self::$descriptors as $code => $descriptor) {
$lifecycle = self::normalizeLifecycle(isset($descriptor['lifecycle']) ? $descriptor['lifecycle'] : null);
$migrations = isset($descriptor['migrations']) ? $descriptor['migrations'] : null;
if (!empty($lifecycle['managed']) || empty($migrations)) {
continue;
}
$basePath = isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '';
$results = array_merge($results, ModuleMigrator::apply($code, $migrations, $basePath));
}
CompiledModuleRegistry::invalidate('adminx');
return $results;
}
/** Состояние миграций встроенных разделов без изменения схемы. */
public static function coreSchemaStatus()
{
$status = array('total' => 0, 'pending' => 0, 'changed' => 0);
foreach (self::$descriptors as $code => $descriptor) {
$lifecycle = self::normalizeLifecycle(isset($descriptor['lifecycle']) ? $descriptor['lifecycle'] : null);
$migrations = isset($descriptor['migrations']) ? $descriptor['migrations'] : null;
if (!empty($lifecycle['managed']) || empty($migrations)) {
continue;
}
$basePath = isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '';
$item = ModuleMigrator::inspect($code, $migrations, $basePath);
$status['total'] += (int) $item['total'];
$status['pending'] += (int) $item['pending'];
$status['changed'] += (int) $item['changed'];
}
return $status;
}
public static function toggle($code, $enabled)
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (empty($meta['installed'])) {
throw new \RuntimeException('Модуль не установлен');
}
if ($enabled) {
self::assertDependencies($descriptor);
} else {
self::assertNoEnabledDependents($code);
}
$status = $enabled ? 'enabled' : 'disabled';
self::writeLifecycleState($descriptor, $status);
self::writeLifecycleEvent($code, $status, $enabled ? 'Модуль включен' : 'Модуль отключен');
self::$disabledCodes = null;
Hooks::action($enabled ? 'module.enabled' : 'module.disabled', new LifecycleEvent('module', $status, $code, array(
'module' => self::refreshLifecycleMeta($code),
), null, array(), 'module_manager'));
return self::refreshLifecycleMeta($code);
}
/** Деинсталлировать модуль вместе с принадлежащими ему данными. */
public static function uninstall($code, $preflightToken = '')
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (empty($meta['installed'])) {
throw new \RuntimeException('Модуль уже не установлен');
}
self::assertNoEnabledDependents($code);
self::assertNoUsedFieldTypes($code);
self::confirmLifecyclePreflight($descriptor, 'uninstall', $preflightToken);
return self::performUninstall($descriptor, $meta);
}
protected static function performUninstall(array $descriptor, array $meta)
{
$code = (string) $descriptor['code'];
Hooks::action('module.uninstalling', new LifecycleEvent('module', 'uninstalling', $code, array(
'module' => $meta,
), null, array(), 'module_manager'));
self::runUninstallChange($descriptor, !empty($meta['enabled']) ? 'enabled' : 'disabled');
self::writeLifecycleEvent($code, 'uninstalled', 'Модуль деинсталлирован');
self::$disabledCodes = null;
Hooks::action('module.uninstalled', new LifecycleEvent('module', 'uninstalled', $code, array(
'module' => self::refreshLifecycleMeta($code),
), null, array(), 'module_manager'));
return self::refreshLifecycleMeta($code);
}
public static function reinstall($code, $preflightToken = '')
{
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (empty($meta['installed'])) {
throw new \RuntimeException('Модуль не установлен');
}
self::assertNoEnabledDependents($code);
self::assertNoUsedFieldTypes($code);
self::confirmLifecyclePreflight($descriptor, 'reinstall', $preflightToken);
self::performUninstall($descriptor, $meta);
return self::install($code);
}
/** Physically remove an already uninstalled package from an allowed module root. */
public static function removePackageFiles($code)
{
$code = (string) $code;
$descriptor = self::managedDescriptor($code);
$meta = self::moduleMeta($code);
if (!empty($meta['installed'])) {
throw new \RuntimeException('Сначала деинсталлируйте модуль');
}
if (empty($descriptor['package']['removable'])) {
throw new \RuntimeException('Физическое удаление файлов для этого модуля запрещено');
}
$basePath = realpath(isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '');
$packagesRoot = realpath(BASEPATH . DS . 'modules');
$adminModulesRoot = realpath(defined('ADMINX_PATH') ? ADMINX_PATH . DS . 'modules' : BASEPATH . DS . 'adminx' . DS . 'modules');
$packageRoot = false;
$registryScopes = array();
if ($basePath && $packagesRoot && basename($basePath) === 'admin') {
$candidate = realpath(dirname($basePath));
if ($candidate && dirname($candidate) === $packagesRoot && basename($candidate) === $code) {
$packageRoot = $candidate;
$registryScopes = array('package_admin', 'public');
}
}
if (!$packageRoot && $basePath && $adminModulesRoot && dirname($basePath) === $adminModulesRoot) {
$packageRoot = $basePath;
$registryScopes = array('adminx');
}
if (!$packageRoot) {
throw new \RuntimeException('Путь пакета не прошёл проверку безопасности');
}
Dir::delete($packageRoot);
if (is_dir($packageRoot)) {
throw new \RuntimeException('Не удалось удалить каталог пакета');
}
ModuleSecretStore::remove($code);
ModuleSettings::purge($code);
ModuleMigrator::forget($code);
\DB::Delete(self::lifecycleTable('module_events'), 'module_code = %s', $code);
\DB::Delete(self::lifecycleTable('modules'), 'code = %s', $code);
unset(self::$modules[$code], self::$descriptors[$code]);
foreach ($registryScopes as $scope) {
CompiledModuleRegistry::invalidate($scope);
}
return array('code' => $code, 'path' => $packageRoot);
}
/** @var string[]|null Кеш кодов выключенных модулей (лениво из БД). */
protected static $disabledCodes = null;
/** Выключен ли модуль (по реестру modules; до миграции реестра — все включены). */
protected static function isDisabled($code)
{
if (self::$disabledCodes === null) {
self::$disabledCodes = [];
if (self::$mode === 'public') {
self::publicLifecycleState('__module_state_warmup__');
foreach (self::$publicLifecycleStates as $moduleCode => $row) {
$status = isset($row['status']) ? (string) $row['status'] : '';
if (in_array($status, array('disabled', 'available'), true)) {
self::$disabledCodes[] = (string) $moduleCode;
}
}
return in_array((string) $code, self::$disabledCodes, true);
}
try {
$table = self::lifecycleTable('modules');
if (self::tableExists($table)) {
$res = \DB::query('SELECT code FROM ' . $table . " WHERE status IN ('disabled', 'available')");
while ($o = $res->getObject()) {
self::$disabledCodes[] = (string) $o->code;
}
}
} catch (\Throwable $e) {
self::$disabledCodes = [];
}
}
return in_array((string) $code, self::$disabledCodes, true);
}
protected static function tableExists($table)
{
try {
$res = \DB::query('SHOW TABLES LIKE %s', $table);
return $res && $res->getValue();
} catch (\Throwable $e) {
return false;
}
}
/** Зарегистрировать settings schema модуля в общем SettingsRegistry. */
protected static function registerSettings($moduleCode, $settings)
{
if (!is_array($settings) || empty($settings)) {
return;
}
SettingsRegistry::addMany($settings, $moduleCode);
}
/** Все глобальные переменные шаблонов (callable уже вычислены). */
public static function viewGlobals()
{
$out = array(
'admin_assets' => array(
'styles' => AdminAssets::styles(),
'scripts' => AdminAssets::scripts(),
),
);
foreach (self::$viewGlobals as $key => $value) {
$out[$key] = is_callable($value) ? call_user_func($value) : $value;
}
return $out;
}
/** Одна глобальная переменная шаблона. */
public static function viewGlobal($key, $default = null)
{
if (!array_key_exists($key, self::$viewGlobals)) {
return $default;
}
$value = self::$viewGlobals[$key];
return is_callable($value) ? call_user_func($value) : $value;
}
public static function publicRoutes()
{
return array_values(array_unique(self::$publicRoutes));
}
public static function publicPrefixes()
{
return array_values(array_unique(self::$publicPrefixes));
}
public static function errors()
{
return self::$errors;
}
public static function summary()
{
return [
'modules' => self::all(),
'errors' => self::errors(),
'public_routes' => self::publicRoutes(),
'public_prefixes' => self::publicPrefixes(),
];
}
protected static function registerPermissions($code, $permissions)
{
if (empty($permissions) || !is_array($permissions) || empty($permissions['items'])) {
return;
}
Permission::add(
isset($permissions['key']) ? $permissions['key'] : $code,
(array) $permissions['items'],
isset($permissions['icon']) ? $permissions['icon'] : '',
isset($permissions['priority']) ? (int) $permissions['priority'] : 10
);
}
protected static function countPermissions($permissions)
{
if (empty($permissions) || !is_array($permissions) || empty($permissions['items']) || !is_array($permissions['items'])) {
return 0;
}
return count($permissions['items']);
}
protected static function countItems($items)
{
return is_array($items) ? count($items) : 0;
}
protected static function countConfig($config)
{
if (empty($config)) {
return 0;
}
if (isset($config['file']) || isset($config['class'])) {
return 1;
}
return is_array($config) ? count($config) : 0;
}
protected static function countFiles($files)
{
if (empty($files)) {
return 0;
}
return is_array($files) ? count($files) : 1;
}
protected static function registerConfig($basePath, $configs)
{
if (empty($configs)) {
return;
}
if (isset($configs['file']) || isset($configs['class'])) {
$configs = [$configs];
}
if (!is_array($configs)) {
return;
}
foreach ($configs as $config) {
if (!is_array($config) || empty($config['file']) || empty($config['class'])) {
continue;
}
$path = self::resolveModulePath($basePath, (string) $config['file']);
$class = (string) $config['class'];
if (!is_file($path) || !class_exists($class) || !method_exists($class, 'load')) {
continue;
}
$data = include $path;
$class::load(is_array($data) ? $data : []);
}
}
protected static function registerAdminAssets($assets)
{
if (empty($assets) || !is_array($assets)) {
return;
}
foreach (array('styles' => 'addStyle', 'scripts' => 'addScript') as $key => $method) {
if (empty($assets[$key]) || !is_array($assets[$key])) {
continue;
}
foreach ($assets[$key] as $asset) {
if (is_string($asset)) {
call_user_func(array(AdminAssets::class, $method), $asset, 100);
continue;
}
if (!is_array($asset) || empty($asset['url'])) {
continue;
}
call_user_func(array(AdminAssets::class, $method), $asset['url'], isset($asset['priority']) ? (int) $asset['priority'] : 100);
}
}
}
protected static function registerMigrations($code, $basePath, $migrations, array $context = array())
{
if (empty($migrations)) {
return;
}
ModuleMigrator::apply($code, $migrations, $basePath, $context);
}
protected static function registerFiles($basePath, $files)
{
if (empty($files)) {
return;
}
if (is_string($files)) {
$files = [$files];
}
if (!is_array($files)) {
return;
}
foreach ($files as $file) {
$file = (string) $file;
if ($file === '') {
continue;
}
$path = self::resolveModulePath($basePath, $file);
if (is_file($path)) {
require_once $path;
}
}
}
/**
* Register route files or compact descriptor definitions.
*
* A definition uses array(method, pattern, handler). Keeping route files
* supported preserves compatibility with modules that build routes
* dynamically.
*/
protected static function registerRoutes($basePath, $routes, array $defaultOptions = array())
{
if (empty($routes)) {
return;
}
if (is_string($routes)) {
self::registerFiles($basePath, $routes);
return;
}
if (!is_array($routes)) {
return;
}
foreach ($routes as $route) {
if (is_string($route)) {
self::registerFiles($basePath, $route);
continue;
}
if (!is_array($route)) {
continue;
}
$method = isset($route['method']) ? $route['method'] : (isset($route[0]) ? $route[0] : '');
$pattern = isset($route['pattern']) ? $route['pattern'] : (isset($route[1]) ? $route[1] : '');
$handler = isset($route['handler']) ? $route['handler'] : (isset($route[2]) ? $route[2] : null);
$options = isset($route['options']) && is_array($route['options'])
? $route['options']
: (isset($route[3]) && is_array($route[3]) ? $route[3] : array());
$options = array_merge($defaultOptions, $options);
$method = strtolower(trim((string) $method));
if ($pattern === '' || $handler === null
|| !in_array($method, array('get', 'post', 'put', 'patch', 'delete', 'any'), true)) {
continue;
}
call_user_func(array(Router::class, $method), (string) $pattern, $handler, $options);
}
}
protected static function adminRouteOptions(array $descriptor, array $admin)
{
$permission = self::routePermission(isset($admin['navigation']) ? $admin['navigation'] : null);
if ($permission === '') {
$permission = self::routePermission(isset($descriptor['navigation']) ? $descriptor['navigation'] : null);
}
if ($permission === '' && !empty($descriptor['admin_extension']['menu'])) {
$permission = self::routePermission($descriptor['admin_extension']['menu']);
}
$permissions = isset($admin['permissions']) ? $admin['permissions'] : (isset($descriptor['permissions']) ? $descriptor['permissions'] : null);
if ($permission === '' && is_array($permissions)) {
$items = isset($permissions['items']) && is_array($permissions['items']) ? $permissions['items'] : $permissions;
foreach ($items as $item) {
$code = is_array($item) && isset($item['code']) ? trim((string) $item['code']) : '';
if (strpos($code, 'view_') === 0) {
$permission = $code;
break;
}
}
}
$options = array('module' => isset($descriptor['code']) ? (string) $descriptor['code'] : '');
if ($permission !== '') {
$options['permission'] = $permission;
}
return $options;
}
protected static function routePermission($items)
{
foreach (is_array($items) ? $items : array() as $item) {
if (!is_array($item)) {
continue;
}
$permission = isset($item['permission']) ? trim((string) $item['permission']) : '';
if ($permission !== '') {
return $permission;
}
$permission = self::routePermission(isset($item['children']) ? $item['children'] : array());
if ($permission !== '') {
return $permission;
}
}
return '';
}
protected static function registerNamespaces($basePath, $namespaces)
{
if (!is_array($namespaces)) { return; }
foreach ($namespaces as $namespace => $path) {
$namespace = trim((string) $namespace, '\\');
$path = self::resolveModulePath($basePath, (string) $path);
if ($namespace !== '' && is_dir($path)) {
Load::regNamespace($namespace, $path);
}
}
}
protected static function resolveModulePath($basePath, $file)
{
if ($file !== '' && $file[0] === '/') {
return $file;
}
return rtrim((string) $basePath, DS) . DS . ltrim((string) $file, DS);
}
protected static function registerNavigation($navigation)
{
if (!empty($navigation) && is_array($navigation)) {
Navigation::addMany($navigation);
}
}
protected static function registerHooks($hooks)
{
if (empty($hooks) || !is_array($hooks)) {
return;
}
foreach ($hooks as $hook) {
if (!is_array($hook) || empty($hook['name']) || !isset($hook['handler'])) {
continue;
}
Hooks::add(
$hook['name'],
$hook['handler'],
isset($hook['priority']) ? (int) $hook['priority'] : 10
);
}
}
protected static function registerFieldTypes($moduleCode, $types, $creatable)
{
if (!is_array($types)) {
return;
}
foreach ($types as $definition) {
$class = is_string($definition)
? $definition
: (is_array($definition) && isset($definition['class']) ? $definition['class'] : '');
if ($class === '') {
continue;
}
$canCreate = is_array($definition) && array_key_exists('creatable', $definition)
? (bool) $definition['creatable'] && (bool) $creatable
: (bool) $creatable;
try {
FieldRegistry::registerClassName($class, 'module:' . (string) $moduleCode, $canCreate);
} catch (\Throwable $e) {
self::$errors[] = 'Field type ' . $class . ': ' . $e->getMessage();
}
}
}
protected static function registerFieldSets($moduleCode, $sets, $available)
{
if (!is_array($sets)) { return; }
try {
FieldSetRegistry::registerMany('module:' . (string) $moduleCode, $sets, (bool) $available);
} catch (\Throwable $e) {
self::$errors[] = 'Field sets ' . (string) $moduleCode . ': ' . $e->getMessage();
}
}
protected static function registerRequestRenderers($moduleCode, $renderers, $available)
{
if (!is_array($renderers)) { return; }
try {
RequestRendererRegistry::registerMany('module:' . (string) $moduleCode, $renderers, (bool) $available);
} catch (\Throwable $e) {
self::$errors[] = 'Request renderers ' . (string) $moduleCode . ': ' . $e->getMessage();
}
}
protected static function registerContentBlocks($moduleCode, $blocks, $available)
{
if (!is_array($blocks)) { return; }
try {
ContentBlockRegistry::registerMany('module:' . (string) $moduleCode, $blocks, (bool) $available);
} catch (\Throwable $e) {
self::$errors[] = 'Content blocks ' . (string) $moduleCode . ': ' . $e->getMessage();
}
}
protected static function assertNoUsedFieldTypes($moduleCode)
{
$codes = FieldRegistry::codesBySource('module:' . (string) $moduleCode);
if (!$codes) {
return;
}
$table = \App\Content\ContentTables::table('rubric_fields');
$used = array();
foreach ($codes as $code) {
try {
$count = (int) \DB::query(
'SELECT COUNT(*) FROM %b WHERE rubric_field_type=%s',
$table,
$code
)->getValue();
} catch (\Throwable $e) {
$count = 0;
}
if ($count > 0) {
$used[$code] = $count;
}
}
if (!$used) {
return;
}
$parts = array();
foreach ($used as $code => $count) {
$parts[] = $code . ' (' . $count . ')';
}
throw new \RuntimeException('Модуль содержит используемые типы полей: ' . implode(', ', $parts));
}
protected static function registerViewGlobals($globals)
{
if (empty($globals) || !is_array($globals)) {
return;
}
foreach ($globals as $key => $value) {
self::$viewGlobals[(string) $key] = $value;
}
}
protected static function registerPublicAccess(array $descriptor)
{
if (!empty($descriptor['public_routes']) && is_array($descriptor['public_routes'])) {
foreach ($descriptor['public_routes'] as $route) {
self::$publicRoutes[] = (string) $route;
}
}
if (!empty($descriptor['public_prefixes']) && is_array($descriptor['public_prefixes'])) {
foreach ($descriptor['public_prefixes'] as $prefix) {
self::$publicPrefixes[] = (string) $prefix;
}
}
}
protected static function normalizeLifecycle($lifecycle)
{
if (!is_array($lifecycle) || empty($lifecycle['managed'])) {
return array('managed' => false);
}
return array(
'managed' => true,
'adopt_existing' => !empty($lifecycle['adopt_existing']),
'install' => isset($lifecycle['install']) ? $lifecycle['install'] : null,
'update' => isset($lifecycle['update']) ? $lifecycle['update'] : null,
'uninstall' => isset($lifecycle['uninstall']) ? $lifecycle['uninstall'] : null,
'preflight' => isset($lifecycle['preflight']) && is_array($lifecycle['preflight'])
? $lifecycle['preflight']
: array(),
);
}
protected static function normalizePreflightOperation($operation)
{
$operation = strtolower(trim((string) $operation));
if (!in_array($operation, array('uninstall', 'reinstall'), true)) {
throw new \InvalidArgumentException('Предварительная проверка для этой lifecycle-операции недоступна');
}
return $operation;
}
protected static function lifecyclePreview(array $descriptor, $operation)
{
$lifecycle = self::normalizeLifecycle(isset($descriptor['lifecycle']) ? $descriptor['lifecycle'] : null);
$handlers = isset($lifecycle['preflight']) ? $lifecycle['preflight'] : array();
$handler = isset($handlers[$operation]) ? $handlers[$operation] : null;
if ($handler === null && $operation === 'reinstall' && isset($handlers['uninstall'])) {
$handler = $handlers['uninstall'];
}
if ($handler === null) {
return null;
}
if (!is_callable($handler)) {
throw new \RuntimeException('Обработчик lifecycle-preflight недоступен');
}
$preview = call_user_func($handler, $descriptor, $operation);
if (!is_array($preview)) {
throw new \RuntimeException('Обработчик lifecycle-preflight должен вернуть массив');
}
return $preview;
}
protected static function confirmLifecyclePreflight(array $descriptor, $operation, $token)
{
$preview = self::lifecyclePreview($descriptor, $operation);
if ($preview === null) {
return;
}
if ((string) $token === '') {
throw new \RuntimeException('Сначала выполните предварительную проверку lifecycle-операции');
}
ModuleLifecyclePreflight::consume($token, $descriptor['code'], $operation, Auth::id(), $preview);
}
protected static function normalizeDependencies($dependencies)
{
$result = array();
foreach (is_array($dependencies) ? $dependencies : array() as $dependency) {
$code = is_array($dependency) && isset($dependency['code'])
? (string) $dependency['code']
: (string) $dependency;
$code = strtolower(trim($code));
if ($code !== '' && preg_match('/^[a-z][a-z0-9_-]{0,63}$/', $code)) {
$result[] = $code;
}
}
return array_values(array_unique($result));
}
protected static function assertDependencies(array $descriptor)
{
$missing = array();
foreach (self::normalizeDependencies(isset($descriptor['requires']) ? $descriptor['requires'] : null) as $code) {
$module = self::moduleMeta($code);
if (!$module || empty($module['installed']) || empty($module['enabled'])) {
$missing[] = self::moduleDisplayName($code);
}
}
if ($missing) {
$message = count($missing) === 1
? 'Сначала установите и включите обязательный модуль '
: 'Сначала установите и включите обязательные модули: ';
throw new \RuntimeException($message . implode(', ', $missing) . '.');
}
}
protected static function assertCoreSchemaReady()
{
$status = self::coreSchemaStatus();
if ((int) $status['pending'] === 0 && (int) $status['changed'] === 0) {
return;
}
throw new \RuntimeException(
'Перед установкой модуля обновите схему ядра в разделе «База данных → Миграции». '
. 'Неприменённых миграций: ' . (int) $status['pending']
. ', изменённых: ' . (int) $status['changed'] . '.'
);
}
protected static function assertNoEnabledDependents($code)
{
$dependents = array();
foreach (self::$descriptors as $candidate => $descriptor) {
if ($candidate === (string) $code
|| !in_array((string) $code, self::normalizeDependencies(isset($descriptor['requires']) ? $descriptor['requires'] : null), true)) {
continue;
}
$module = self::moduleMeta($candidate);
if ($module && !empty($module['installed']) && !empty($module['enabled'])) {
$dependents[] = self::moduleDisplayName($candidate);
}
}
if ($dependents) {
$message = count($dependents) === 1
? 'Сначала отключите зависимый модуль '
: 'Сначала отключите зависимые модули: ';
throw new \RuntimeException($message . implode(', ', $dependents) . '.');
}
}
protected static function moduleDisplayName($code)
{
$code = strtolower(trim((string) $code));
$descriptor = isset(self::$descriptors[$code]) && is_array(self::$descriptors[$code])
? self::$descriptors[$code]
: array();
$name = trim(isset($descriptor['name']) ? (string) $descriptor['name'] : '');
return '«' . ($name !== '' ? $name : $code) . '»';
}
protected static function resolveLifecycleState($code, array $descriptor, array $lifecycle)
{
if (empty($lifecycle['managed'])) {
return array(
'installed' => true,
'enabled' => !self::isDisabled($code),
'status' => self::isDisabled($code) ? 'disabled' : 'enabled',
'version' => isset($descriptor['version']) ? (string) $descriptor['version'] : '',
'recoverable' => false,
);
}
$row = self::lifecycleState($code, self::$mode !== 'public');
if (!$row && self::$mode !== 'public' && !empty($lifecycle['adopt_existing'])) {
self::writeLifecycleState($descriptor, 'enabled');
$row = array('status' => 'enabled', 'version' => isset($descriptor['version']) ? (string) $descriptor['version'] : '');
}
$status = $row && isset($row['status']) ? (string) $row['status'] : 'available';
return array(
'installed' => in_array($status, array('enabled', 'disabled'), true),
'enabled' => $status === 'enabled',
'status' => $status,
'version' => $row && isset($row['version']) ? (string) $row['version'] : '',
'recoverable' => $status === 'dirty',
'operation' => $row && isset($row['operation']) ? (string) $row['operation'] : '',
'previous_status' => $row && isset($row['previous_status']) ? (string) $row['previous_status'] : '',
'last_error' => $row && isset($row['last_error']) ? (string) $row['last_error'] : '',
);
}
protected static function lifecycleState($code, $allowEnsure)
{
if (self::$publicLifecycleStates === null) {
self::$publicLifecycleStates = array();
try {
self::loadLifecycleStates();
} catch (\Throwable $e) {
if ($allowEnsure) {
self::ensureLifecycleTable();
self::loadLifecycleStates();
}
}
}
return isset(self::$publicLifecycleStates[$code]) ? self::$publicLifecycleStates[$code] : null;
}
protected static function loadLifecycleStates()
{
$rows = \DB::query(
'SELECT * FROM ' . self::lifecycleTable('modules')
)->getAll();
foreach ($rows as $row) {
if (!empty($row['code'])) {
self::$publicLifecycleStates[(string) $row['code']] = $row;
}
}
}
protected static function ensureLifecycleTable()
{
if (self::$lifecycleTableEnsured) {
return;
}
\DB::query(
'CREATE TABLE IF NOT EXISTS ' . self::lifecycleTable('modules') . " (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
version VARCHAR(50) NOT NULL DEFAULT '',
schema_version INT UNSIGNED NOT NULL DEFAULT 0,
status VARCHAR(30) NOT NULL DEFAULT 'available',
base_path VARCHAR(500) NULL,
description TEXT NULL,
installed_at DATETIME NULL,
enabled_at DATETIME NULL,
disabled_at DATETIME NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_code (code),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
\DB::query(
'CREATE TABLE IF NOT EXISTS ' . self::lifecycleTable('module_events') . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
module_code VARCHAR(100) NOT NULL,
event_type VARCHAR(50) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'ok',
message TEXT NULL,
context LONGTEXT NULL,
created_by INT UNSIGNED NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_module_created (module_code, created_at),
KEY idx_type (event_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$modules = self::lifecycleTable('modules');
$columns = array(
'previous_status' => "VARCHAR(30) NOT NULL DEFAULT '' AFTER disabled_at",
'operation' => "VARCHAR(20) NOT NULL DEFAULT '' AFTER previous_status",
'operation_started_at' => 'DATETIME NULL AFTER operation',
'failed_at' => 'DATETIME NULL AFTER operation_started_at',
'last_error' => 'TEXT NULL AFTER failed_at',
);
foreach ($columns as $name => $definition) {
$exists = \DB::query(
'SELECT 1 FROM information_schema.COLUMNS'
. ' WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s AND COLUMN_NAME=%s LIMIT 1',
$modules,
$name
)->getValue();
if (!$exists) { \DB::query('ALTER TABLE ' . $modules . ' ADD COLUMN ' . $name . ' ' . $definition); }
}
self::$lifecycleTableEnsured = true;
}
protected static function runLifecycleChange(array $descriptor, $operation, $previousStatus)
{
$code = (string) $descriptor['code'];
$transition = $operation === 'install' ? 'installing' : 'updating';
self::writeLifecycleState($descriptor, $transition, array(
'previous_status' => (string) $previousStatus,
'operation' => (string) $operation,
));
self::writeLifecycleEvent($code, $transition, 'Операция модуля начата', 'running');
try {
$basePath = isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '';
self::registerMigrations(
$code,
$basePath,
isset($descriptor['migrations']) ? $descriptor['migrations'] : null,
array('operation' => $operation, 'actor_id' => Auth::id())
);
self::invokeLifecycleHandler($descriptor, $operation);
self::registerPermissions($code, self::adminValue($descriptor, 'permissions'));
Permission::syncRegistryToDb();
$stableStatus = $operation === 'install' ? 'enabled' : (string) $previousStatus;
self::writeLifecycleState($descriptor, $stableStatus);
} catch (\Throwable $e) {
self::markLifecycleFailure($descriptor, $operation, $previousStatus, $e);
throw $e;
}
}
protected static function runUninstallChange(array $descriptor, $previousStatus)
{
$code = (string) $descriptor['code'];
self::writeLifecycleState($descriptor, 'uninstalling', array(
'previous_status' => (string) $previousStatus,
'operation' => 'uninstall',
));
self::writeLifecycleEvent($code, 'uninstalling', 'Деинсталляция модуля начата', 'running');
try {
$hasUninstallHandler = !empty($descriptor['lifecycle']['uninstall']);
self::invokeLifecycleHandler($descriptor, 'uninstall');
ModuleSettings::purge($code);
ModuleSecretStore::remove($code);
PublicRouteRegistry::removeOwner($code);
self::removeModulePermissions($descriptor);
if ($hasUninstallHandler) { ModuleMigrator::forget($code); }
self::writeLifecycleState($descriptor, 'available');
} catch (\Throwable $e) {
self::markLifecycleFailure($descriptor, 'uninstall', $previousStatus, $e);
throw $e;
}
}
protected static function markLifecycleFailure(array $descriptor, $operation, $previousStatus, \Throwable $error)
{
self::writeLifecycleState($descriptor, 'dirty', array(
'previous_status' => (string) $previousStatus,
'operation' => (string) $operation,
'last_error' => $error->getMessage(),
));
self::writeLifecycleEvent(
(string) $descriptor['code'],
$operation . '_failed',
$error->getMessage(),
'failed'
);
}
protected static function writeLifecycleState(array $descriptor, $status, array $context = array())
{
self::ensureLifecycleTable();
self::$publicLifecycleStates = null;
$code = (string) $descriptor['code'];
$now = date('Y-m-d H:i:s');
$table = self::lifecycleTable('modules');
$existing = \DB::query('SELECT * FROM ' . $table . ' WHERE code = %s LIMIT 1', $code)->getAssoc();
$stable = in_array((string) $status, array('available', 'enabled', 'disabled'), true);
$data = array(
'name' => isset($descriptor['name']) ? (string) $descriptor['name'] : $code,
'version' => isset($descriptor['version']) ? (string) $descriptor['version'] : '',
'status' => (string) $status,
'base_path' => isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '',
'description' => isset($descriptor['description']) ? (string) $descriptor['description'] : '',
'installed_at' => $status === 'available' ? null : ($existing && !empty($existing['installed_at']) ? $existing['installed_at'] : $now),
'enabled_at' => $status === 'available' ? null : ($status === 'enabled' ? $now : (isset($existing['enabled_at']) ? $existing['enabled_at'] : null)),
'disabled_at' => $status === 'available' ? null : ($status === 'disabled' ? $now : (isset($existing['disabled_at']) ? $existing['disabled_at'] : null)),
'previous_status' => $stable ? '' : (isset($context['previous_status']) ? (string) $context['previous_status'] : ''),
'operation' => $stable ? '' : (isset($context['operation']) ? (string) $context['operation'] : ''),
'operation_started_at' => $stable ? null : ($existing && !empty($existing['operation_started_at']) ? $existing['operation_started_at'] : $now),
'failed_at' => $status === 'dirty' ? $now : null,
'last_error' => $status === 'dirty' && isset($context['last_error']) ? (string) $context['last_error'] : null,
'updated_at' => $now,
);
if ($existing) {
\DB::Update($table, $data, 'id = %i', (int) $existing['id']);
} else {
$data['code'] = $code;
\DB::Insert($table, $data);
}
}
protected static function writeLifecycleEvent($code, $event, $message, $status = 'ok')
{
self::ensureLifecycleTable();
\DB::Insert(self::lifecycleTable('module_events'), array(
'module_code' => (string) $code,
'event_type' => (string) $event,
'status' => (string) $status,
'message' => (string) $message,
'context' => null,
'created_by' => Auth::id() ?: null,
'created_at' => date('Y-m-d H:i:s'),
));
}
/** Служебные таблицы модулей общие для admin/public runtime. */
protected static function lifecycleTable($suffix)
{
$suffix = (string) $suffix;
if (!in_array($suffix, array('modules', 'module_events'), true)) {
throw new \InvalidArgumentException('Некорректная lifecycle-таблица модулей');
}
return SystemTables::table($suffix);
}
protected static function managedDescriptor($code)
{
$descriptor = self::descriptor((string) $code);
if (!$descriptor || empty($descriptor['lifecycle']['managed'])) {
throw new \RuntimeException('Нативный управляемый модуль не найден');
}
return $descriptor;
}
protected static function moduleMeta($code)
{
return isset(self::$modules[$code]) ? self::$modules[$code] : null;
}
protected static function refreshLifecycleMeta($code)
{
$descriptor = self::descriptor($code);
$lifecycle = self::normalizeLifecycle(isset($descriptor['lifecycle']) ? $descriptor['lifecycle'] : null);
$state = self::resolveLifecycleState($code, $descriptor, $lifecycle);
self::$modules[$code]['installed'] = $state['installed'];
self::$modules[$code]['enabled'] = $state['enabled'];
self::$modules[$code]['status'] = $state['status'];
self::$modules[$code]['db_version'] = $state['version'];
self::$modules[$code]['recoverable'] = !empty($state['recoverable']);
self::$modules[$code]['operation'] = isset($state['operation']) ? $state['operation'] : '';
self::$modules[$code]['previous_status'] = isset($state['previous_status']) ? $state['previous_status'] : '';
self::$modules[$code]['last_error'] = isset($state['last_error']) ? $state['last_error'] : '';
return self::$modules[$code];
}
protected static function adminValue(array $descriptor, $key)
{
$admin = isset($descriptor['admin']) && is_array($descriptor['admin']) ? $descriptor['admin'] : array();
return isset($admin[$key]) ? $admin[$key] : (isset($descriptor[$key]) ? $descriptor[$key] : null);
}
protected static function invokeLifecycleHandler(array $descriptor, $operation)
{
$lifecycle = isset($descriptor['lifecycle']) && is_array($descriptor['lifecycle']) ? $descriptor['lifecycle'] : array();
if (empty($lifecycle[$operation])) {
return;
}
$handler = $lifecycle[$operation];
if (is_callable($handler)) {
call_user_func($handler, $descriptor);
return;
}
$files = is_array($handler) ? $handler : array($handler);
$basePath = isset($descriptor['_base_path']) ? (string) $descriptor['_base_path'] : '';
foreach ($files as $file) {
$path = self::resolveModulePath($basePath, (string) $file);
if (!is_file($path)) {
throw new \RuntimeException('Lifecycle SQL file not found: ' . $path);
}
$sql = ModuleMigrator::expandPrefixes((string) file_get_contents($path));
foreach (ModuleMigrator::splitSql($sql) as $statement) {
\DB::query($statement);
}
}
}
protected static function removeModulePermissions(array $descriptor)
{
$permissions = self::adminValue($descriptor, 'permissions');
if (!is_array($permissions) || empty($permissions['items'])) {
return;
}
foreach ((array) $permissions['items'] as $permission) {
$code = is_array($permission) && isset($permission['code']) ? (string) $permission['code'] : (string) $permission;
if ($code === '') {
continue;
}
if (self::tableExists(SystemTables::table('role_permissions'))) {
\DB::Delete(SystemTables::table('role_permissions'), 'permission_code = %s', $code);
}
if (self::tableExists(SystemTables::table('permissions'))) {
\DB::Delete(SystemTables::table('permissions'), 'code = %s', $code);
}
}
}
protected function __clone()
{
//
}
protected function __wakeup()
{
//
}
}