Вход Регистрация
Файл: app/Models/Module.php
Строк: 225
<?php

declare(strict_types=1);

namespace 
AppModels;

use 
CarbonCarbonImmutable;
use 
Exception;
use 
IlluminateDatabaseEloquentModel;
use 
IlluminateSupportFacadesArtisan;
use 
IlluminateSupportFacadesCache;
use 
IlluminateSupportFacadesDB;
use 
IlluminateSupportFacadesFile;
use 
IlluminateSupportStr;

/**
 * Class Module
 *
 * @property int             $id
 * @property string          $name
 * @property string          $version
 * @property bool            $active
 * @property CarbonImmutable $updated_at
 * @property CarbonImmutable $created_at
 */
class Module extends Model
{
    
/**
     * Assets modules path
     */
    
protected const string ASSETS_PATH '/assets/modules/';

    
/**
     * The attributes that aren't mass assignable.
     */
    
protected $guarded = [];

    
/**
     * Get the attributes that should be cast.
     */
    
protected function casts(): array
    {
        return [
            
'active' => 'bool',
        ];
    }

    
/**
     * Выполняет применение миграции
     */
    
public function migrate(): void
    
{
        
$migrationPath base_path('modules/' $this->name '/database/migrations');

        if (
file_exists($migrationPath)) {
            
Artisan::call('migrate', [
                
'--force'    => true,
                
'--realpath' => true,
                
'--path'     => $migrationPath,
            ]);
        }
    }

    
/**
     * Выполняет откат миграций
     */
    
public function rollback(): void
    
{
        
$migrationPath base_path('modules/' $this->name '/database/migrations');

        if (
file_exists($migrationPath)) {
            
$migrator app('migrator');
            
$nextBatchNumber $migrator->getRepository()->getNextBatchNumber();
            
$migrationNames array_keys($migrator->getMigrationFiles($migrationPath));

            
DB::table(config('database.migrations.table'))
                ->
whereIn('migration'$migrationNames)
                ->
update(['batch' => $nextBatchNumber]);

            
Artisan::call('migrate:rollback', [
                
'--force'    => true,
                
'--realpath' => true,
                
'--path'     => $migrationPath,
            ]);
        }
    }

    
/**
     * Создает симлинки модулей
     */
    
public function createSymlink(): void
    
{
        
$originPath public_path($this->getLinkName());
        if (
file_exists($originPath)) {
            return;
        }

        
$assetsPath base_path('modules/' $this->name '/resources/assets');
        if (! 
file_exists($assetsPath)) {
            return;
        }

        
File::link($assetsPath$originPath);
    }

    
/**
     * Удаляет симлинки модулей
     */
    
public function deleteSymlink(): void
    
{
        
$originPath public_path($this->getLinkName());
        if (! 
file_exists($originPath)) {
            return;
        }

        
File::delete($originPath);
    }

    
/**
     * Синхронизирует все активные модули с ядром: симлинки и публикация файлов.
     *
     * Каждый модуль обёрнут в try/catch — битый модуль (ошибка в module.php,
     * проблема с правами на симлинк и т.п.) не роняет синхронизацию остальных.
     * Полная синхронизация делает публикацию независимой от порядка установки:
     * напр. перевод модуля-языка подмешается в Форум, даже если Форум поставили позже.
     *
     * @return array<string, string> [имя модуля => текст ошибки]
     */
    
public static function syncAll(): array
    {
        
$failed = [];

        foreach (
self::query()->where('active'true)->get() as $module) {
            try {
                
$module->createSymlink();
                
$module->publish();
            } catch (
Throwable $e) {
                
$failed[$module->name] = $e->getMessage();
                
report($e);
            }
        }

        
clearCache(['modules''settings']);

        return 
$failed;
    }

    
/**
     * Копирует файлы модуля в директории движка
     */
    
public function publish(): void
    
{
        foreach (
$this->getPublishMap() as $from => $to) {
            if (! 
file_exists($from)) {
                continue;
            }

            if (
is_dir($from)) {
                
File::copyDirectory($from$to);
            } else {
                
File::ensureDirectoryExists(dirname($to));
                
File::copy($from$to);
            }
        }
    }

    
/**
     * Удаляет ранее скопированные файлы модуля
     */
    
public function unpublish(): void
    
{
        foreach (
$this->getPublishMap() as $from => $to) {
            if (
is_dir($from)) {
                
File::deleteDirectory($to);
            } elseif (
is_file($from)) {
                
File::delete($to);
            }
        }
    }

    
/**
     * Карта публикации файлов модуля [источник => назначение]
     */
    
private function getPublishMap(): array
    {
        
$configFile base_path('modules/' $this->name '/module.php');
        if (! 
file_exists($configFile)) {
            return [];
        }

        
$config = include $configFile;

        
$map = [];
        foreach (
$config['publish'] ?? [] as $from => $to) {
            
// Защита от выхода за пределы директорий
            
if (str_contains($from'..') || str_contains($to'..')) {
                continue;
            }

            
// Публикация в другой модуль только если он есть на диске
            // (напр. модуль-язык подмешивает перевод в modules/Forum/...)
            
if (preg_match('#^modules/([^/]+)/#'$to$match)
                && ! 
is_dir(base_path('modules/' $match[1]))) {
                continue;
            }

            
$map[base_path('modules/' $this->name '/' $from)] = base_path($to);
        }

        return 
$map;
    }

    
/**
     * Получает название директории для симлинка
     */
    
public function getLinkName(): string
    
{
        return 
self::ASSETS_PATH Str::plural(strtolower($this->name));
    }

    
/**
     * Получает название директории для симлинка из пути
     */
    
public static function getLinkNameByPath(string $modulePath): string
    
{
        return 
self::ASSETS_PATH Str::plural(strtolower(basename($modulePath)));
    }

    
/**
     * Количество установленных модулей с доступным обновлением
     */
    
public static function updatesCount(): int
    
{
        
$installed self::query()->pluck('version''name')->all();
        if (! 
$installed) {
            return 
0;
        }

        
$registry ModuleRegistry::getAvailableModules();

        
$count 0;
        foreach (
$installed as $name => $version) {
            
$configFile base_path('modules/' $name '/module.php');
            if (! 
file_exists($configFile)) {
                continue;
            }

            
$config = include $configFile;
            
$localVersion $config['version'] ?? null;
            
$registryVersion $registry[$name]['version'] ?? null;

            
$hasUpdate = ($localVersion && version_compare($localVersion$version'>'))
                || (
$registryVersion && version_compare($registryVersion$version'>'));

            if (
$hasUpdate) {
                
$count++;
            }
        }

        return 
$count;
    }

    
/**
     * Get enabled modules
     *
     * Кеш привязан к версии движка: после обновления ядра структура данных
     * могла измениться, поэтому устаревший кеш пересобирается
     */
    
public static function getEnabledModules(): array
    {
        
$cached Cache::get('modules', []);

        if ((
$cached['version'] ?? null) === ROTOR_VERSION && ! empty($cached['modules'])) {
            return 
$cached['modules'];
        }

        
$modules self::loadEnabledModules();

        if (
$modules !== []) {
            
Cache::forever('modules', [
                
'version' => ROTOR_VERSION,
                
'modules' => $modules,
            ]);
        }

        return 
$modules;
    }

    
/**
     * Загружает активные модули с их файловой структурой
     */
    
private static function loadEnabledModules(): array
    {
        try {
            
$names self::query()
                ->
where('active'true)
                ->
pluck('name')
                ->
all();
        } catch (
Exception) {
            return [];
        }

        
$result = [];
        foreach (
$names as $name) {
            
$result[$name] = [
                
'files'  => self::scanModuleFiles($name),
                
'config' => self::loadModuleConfig($name),
            ];
        }

        return 
$result;
    }

    
/**
     * Загружает config модуля
     */
    
private static function loadModuleConfig(string $name): ?array
    {
        
$configFile base_path('modules/' $name '/config.php');
        if (! 
file_exists($configFile)) {
            return 
null;
        }

        
$config = include $configFile;

        return 
is_array($config) ? $config null;
    }

    
/**
     * Сканирует наличие файлов модуля
     */
    
private static function scanModuleFiles(string $name): array
    {
        
$base base_path('modules/' $name '/');

        return [
            
'views'      => is_dir($base 'resources/views'),
            
'lang'       => is_dir($base 'resources/lang'),
            
'helpers'    => file_exists($base 'helpers.php'),
            
'hooks'      => file_exists($base 'hooks.php'),
            
'routes'     => file_exists($base 'routes.php'),
            
'middleware' => file_exists($base 'middleware.php'),
            
'module'     => file_exists($base 'module.php'),
            
'commands'   => array_map(
                static 
fn ($file) => 'Modules\' . $name . '\Console\' . basename($file, '.php'),
                glob($base . '
Console/*.php') ?: []
            ),
        ];
    }
}
Онлайн: 1
Реклама