Skip to Content
Разработка модулейУправление зависимостями Composer

Управление зависимостями Composer

Flute CMS использует Composer для зависимостей всего проекта. У модулей не обязательно иметь composer.json: в основном зависимости указываются в корневом composer.json. Если в модуле есть собственный composer.json, ModuleManager::runComposerInstall() выполнит composer install из корня проекта (только если файл существует), но никакой специальной магии с extra.flute не применяется.

Что реально используется:

  • Поле extra.flute никак не обрабатывается ядром: зависимости и метаданные берутся из module.json
  • Автозагрузка PSR-4 для модулей настраивается через корневой composer ("Flute\\": "app/"), а модульные composer.json не мержатся автоматически в autoload
  • Если модулю нужны сторонние пакеты, добавляйте их в корневой composer.json либо держите отдельный composer.json в модуле и запускайте composer install вручную/через ModuleManager::runComposerInstall()

Структура composer.json модуля

Основная структура

{ "name": "flute/blog-module", "description": "Blog module for Flute CMS", "version": "1.0.0", "type": "flute-module", "license": "MIT", "authors": [ { "name": "Flute Team", "email": "[email protected]" } ], "require": { "php": "^8.1", "flute/cms": "^1.0", "cycle/orm": "^2.0", "symfony/http-foundation": "^6.0", "illuminate/support": "^10.0" }, "require-dev": { "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.10" }, "autoload": { "psr-4": { "Flute\\Modules\\Blog\\": "src/" } }, "autoload-dev": { "psr-4": { "Flute\\Modules\\Blog\\Tests\\": "tests/" } }, "extra": { "flute": { "module-name": "Blog", "install-path": "app/Modules/Blog", "assets-path": "Resources/assets", "migrations-path": "database/migrations", "config-files": [ "config/blog.php" ] } }, "scripts": { "post-install-cmd": [ "Flute\\Modules\\Blog\\Installer::postInstall" ], "post-update-cmd": [ "Flute\\Modules\\Blog\\Installer::postUpdate" ], "test": "phpunit", "analyse": "phpstan analyse" }, "config": { "sort-packages": true, "optimize-autoloader": true }, "minimum-stability": "stable", "prefer-stable": true }

Управление зависимостями

Сервис управления зависимостями

<?php namespace Flute\Modules\Blog\Services; use Composer\Composer; use Composer\Factory; use Composer\IO\NullIO; use Composer\Json\JsonFile; use Composer\Package\Version\VersionParser; class DependencyManager { protected string $modulePath; protected array $composerData; public function __construct(string $modulePath) { $this->modulePath = $modulePath; $this->loadComposerData(); } /** * Загрузка данных из composer.json */ protected function loadComposerData(): void { $composerFile = $this->modulePath . '/composer.json'; if (!file_exists($composerFile)) { throw new \Exception("composer.json not found in {$this->modulePath}"); } $jsonFile = new JsonFile($composerFile); $this->composerData = $jsonFile->read(); } /** * Проверка зависимостей */ public function checkDependencies(): array { $errors = []; $warnings = []; // Проверка PHP версии if (isset($this->composerData['require']['php'])) { $phpConstraint = $this->composerData['require']['php']; if (!$this->checkPhpVersion($phpConstraint)) { $errors[] = "PHP version {$phpConstraint} required, current: " . PHP_VERSION; } } // Проверка зависимостей Composer if (isset($this->composerData['require'])) { foreach ($this->composerData['require'] as $package => $constraint) { if ($package === 'php') continue; $check = $this->checkPackageDependency($package, $constraint); if (!$check['satisfied']) { if ($check['installed']) { $warnings[] = "Package {$package} version {$check['installed']} does not satisfy {$constraint}"; } else { $errors[] = "Package {$package} is not installed"; } } } } // Проверка специфичных зависимостей Flute if (isset($this->composerData['extra']['flute']['dependencies'])) { $fluteDeps = $this->composerData['extra']['flute']['dependencies']; $fluteErrors = $this->checkFluteDependencies($fluteDeps); $errors = array_merge($errors, $fluteErrors); } return [ 'valid' => empty($errors), 'errors' => $errors, 'warnings' => $warnings ]; } /** * Проверка версии PHP */ protected function checkPhpVersion(string $constraint): bool { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($constraint); return $constraint->matches($versionParser->parseConstraints(PHP_VERSION)); } /** * Проверка зависимости пакета */ protected function checkPackageDependency(string $package, string $constraint): array { $installedPackages = $this->getInstalledPackages(); if (!isset($installedPackages[$package])) { return [ 'satisfied' => false, 'installed' => null ]; } $installedVersion = $installedPackages[$package]['version']; $versionParser = new VersionParser(); $requiredConstraint = $versionParser->parseConstraints($constraint); $installedConstraint = $versionParser->parseConstraints($installedVersion); return [ 'satisfied' => $requiredConstraint->matches($installedConstraint), 'installed' => $installedVersion ]; } /** * Получение установленных пакетов */ protected function getInstalledPackages(): array { $vendorDir = base_path('vendor'); $installedFile = $vendorDir . '/composer/installed.json'; if (!file_exists($installedFile)) { return []; } $installedData = json_decode(file_get_contents($installedFile), true); if (!$installedData || !isset($installedData['packages'])) { return []; } $packages = []; foreach ($installedData['packages'] as $package) { $packages[$package['name']] = [ 'version' => $package['version'], 'description' => $package['description'] ?? '' ]; } return $packages; } /** * Проверка специфичных зависимостей Flute */ protected function checkFluteDependencies(array $dependencies): array { $errors = []; // Проверка версии Flute if (isset($dependencies['flute'])) { if (!$this->checkFluteVersion($dependencies['flute'])) { $errors[] = "Flute CMS version {$dependencies['flute']} required"; } } // Проверка модулей if (isset($dependencies['modules'])) { foreach ($dependencies['modules'] as $module => $version) { if (!$this->checkModuleDependency($module, $version)) { $errors[] = "Module {$module} version {$version} required"; } } } // Проверка расширений PHP if (isset($dependencies['extensions'])) { foreach ($dependencies['extensions'] as $extension) { if (!extension_loaded($extension)) { $errors[] = "PHP extension {$extension} is required"; } } } return $errors; } /** * Проверка версии Flute */ protected function checkFluteVersion(string $constraint): bool { $currentVersion = config('app.version', '1.0.0'); $versionParser = new VersionParser(); $requiredConstraint = $versionParser->parseConstraints($constraint); $currentConstraint = $versionParser->parseConstraints($currentVersion); return $requiredConstraint->matches($currentConstraint); } /** * Проверка зависимости модуля */ protected function checkModuleDependency(string $moduleName, string $constraint): bool { $moduleManager = app(\Flute\Core\ModulesManager\ModuleManager::class); $modules = $moduleManager->getAllModules(); if (!isset($modules[$moduleName])) { return false; } $moduleVersion = $modules[$moduleName]['version'] ?? '0.0.0'; $versionParser = new VersionParser(); $requiredConstraint = $versionParser->parseConstraints($constraint); $moduleConstraint = $versionParser->parseConstraints($moduleVersion); return $requiredConstraint->matches($moduleConstraint); } /** * Установка зависимостей */ public function installDependencies(): bool { try { $this->runComposerCommand('install', ['--no-dev', '--optimize-autoloader']); logs('composer')->info('Dependencies installed for module: ' . basename($this->modulePath)); return true; } catch (\Exception $e) { logs('composer')->error('Failed to install dependencies', [ 'module' => basename($this->modulePath), 'error' => $e->getMessage() ]); return false; } } /** * Обновление зависимостей */ public function updateDependencies(): bool { try { $this->runComposerCommand('update'); logs('composer')->info('Dependencies updated for module: ' . basename($this->modulePath)); return true; } catch (\Exception $e) { logs('composer')->error('Failed to update dependencies', [ 'module' => basename($this->modulePath), 'error' => $e->getMessage() ]); return false; } } /** * Запуск команды Composer */ protected function runComposerCommand(string $command, array $args = []): void { $composerPath = base_path('composer.phar'); if (!file_exists($composerPath)) { $composerPath = 'composer'; } $cmd = [$composerPath, $command]; $cmd = array_merge($cmd, $args); $process = new \Symfony\Component\Process\Process($cmd, $this->modulePath); $process->setTimeout(300); // 5 минут $process->run(); if (!$process->isSuccessful()) { throw new \Exception("Composer command failed: " . $process->getErrorOutput()); } } /** * Генерация composer.json */ public function generateComposerJson(array $config): void { $defaultConfig = [ 'name' => 'flute/' . strtolower($config['module_name'] ?? 'module') . '-module', 'description' => $config['description'] ?? 'Flute CMS Module', 'version' => $config['version'] ?? '1.0.0', 'type' => 'flute-module', 'license' => 'MIT', 'authors' => [ [ 'name' => $config['author'] ?? 'Flute Team', 'email' => $config['email'] ?? '[email protected]' ] ], 'require' => [ 'php' => '^8.1', 'flute/cms' => '^1.0' ], 'autoload' => [ 'psr-4' => [ 'Flute\\Modules\\' . ($config['module_name'] ?? 'Module') . '\\' => 'src/' ] ], 'extra' => [ 'flute' => [ 'module-name' => $config['module_name'] ?? 'Module', 'install-path' => 'app/Modules/' . ($config['module_name'] ?? 'Module') ] ] ]; $finalConfig = array_merge($defaultConfig, $config); $jsonFile = new JsonFile($this->modulePath . '/composer.json'); $jsonFile->write($finalConfig); } }

Примечание: приведённый менеджер — пример. В реальном проекте проверки зависимостей делает ModuleDependencies, а Composer-пакеты устанавливаются из корня.

Установка модулей через Composer

Пользовательский установщик

<?php namespace Flute\Modules\Blog; use Composer\Script\Event; use Flute\Modules\Blog\Services\DependencyManager; class Installer { /** * Пост-установочный скрипт */ public static function postInstall(Event $event): void { $io = $event->getIO(); $composer = $event->getComposer(); $io->write('<info>Installing Blog module...</info>'); try { self::runInstallation($composer, $io); $io->write('<info>Blog module installed successfully!</info>'); } catch (\Exception $e) { $io->writeError('<error>Installation failed: ' . $e->getMessage() . '</error>'); throw $e; } } /** * Пост-обновный скрипт */ public static function postUpdate(Event $event): void { $io = $event->getIO(); $composer = $event->getComposer(); $io->write('<info>Updating Blog module...</info>'); try { self::runUpdate($composer, $io); $io->write('<info>Blog module updated successfully!</info>'); } catch (\Exception $e) { $io->writeError('<error>Update failed: ' . $e->getMessage() . '</error>'); throw $e; } } /** * Запуск установки */ protected static function runInstallation($composer, $io): void { $modulePath = dirname(__DIR__, 2); // Путь к модулю $dependencyManager = new DependencyManager($modulePath); // Проверка зависимостей $io->write('Checking dependencies...'); $result = $dependencyManager->checkDependencies(); if (!$result['valid']) { throw new \Exception('Dependencies not satisfied: ' . implode(', ', $result['errors'])); } if (!empty($result['warnings'])) { foreach ($result['warnings'] as $warning) { $io->write('<warning>' . $warning . '</warning>'); } } // Создание необходимых директорий self::createDirectories($modulePath, $io); // Копирование файлов конфигурации self::copyConfigFiles($modulePath, $io); // Запуск миграций self::runMigrations($modulePath, $io); // Публикация ресурсов self::publishAssets($modulePath, $io); // Регистрация в системе self::registerModule($modulePath, $io); } /** * Создание директорий */ protected static function createDirectories(string $modulePath, $io): void { $directories = [ 'Resources/views/layouts', 'Resources/views/pages', 'Resources/views/components', 'Resources/assets/scss', 'Resources/assets/js', 'Resources/assets/images', 'Resources/config', 'database/migrations', 'database/factories', 'database/seeders', 'src/Controllers', 'src/Services', 'src/Events', 'src/Listeners', 'tests' ]; foreach ($directories as $directory) { $fullPath = $modulePath . '/' . $directory; if (!is_dir($fullPath)) { mkdir($fullPath, 0755, true); $io->write("Created directory: {$directory}"); } } } /** * Копирование файлов конфигурации */ protected static function copyConfigFiles(string $modulePath, $io): void { $configFiles = [ 'config/blog.php' => 'blog.php', 'config/assets.php' => 'assets.php' ]; foreach ($configFiles as $source => $destination) { $sourcePath = $modulePath . '/Resources/' . $source; $destPath = config_path($destination); if (file_exists($sourcePath) && !file_exists($destPath)) { copy($sourcePath, $destPath); $io->write("Copied config file: {$destination}"); } } } /** * Запуск миграций */ protected static function runMigrations(string $modulePath, $io): void { $io->write('Running migrations...'); // Запуск миграций через Flute $migrationFiles = glob($modulePath . '/database/migrations/*.php'); foreach ($migrationFiles as $migrationFile) { $migrationClass = basename($migrationFile, '.php'); $io->write("Running migration: {$migrationClass}"); // Здесь должна быть логика запуска миграции // В реальном коде это будет интегрировано с системой миграций Flute } } /** * Публикация ресурсов */ protected static function publishAssets(string $modulePath, $io): void { $io->write('Publishing assets...'); $assetManager = new \Flute\Modules\Blog\Services\AssetManager(); $assetManager->publish(); $io->write('Assets published successfully'); } /** * Регистрация модуля */ protected static function registerModule(string $modulePath, $io): void { $io->write('Registering module...'); $moduleManager = app(\Flute\Core\ModulesManager\ModuleManager::class); $moduleName = basename($modulePath); $moduleManager->activateModule($moduleName); $io->write('Module registered and activated'); } /** * Запуск обновления */ protected static function runUpdate($composer, $io): void { $modulePath = dirname(__DIR__, 2); $dependencyManager = new DependencyManager($modulePath); // Проверка зависимостей $result = $dependencyManager->checkDependencies(); if (!$result['valid']) { $io->writeError('<error>Cannot update: dependencies not satisfied</error>'); return; } // Обновление ресурсов self::publishAssets($modulePath, $io); // Запуск миграций обновления self::runUpdateMigrations($modulePath, $io); } /** * Запуск миграций обновления */ protected static function runUpdateMigrations(string $modulePath, $io): void { $io->write('Running update migrations...'); // Логика для миграций обновления // Проверка версии модуля и запуск соответствующих миграций } }

Разрешение конфликтов зависимостей

Сервис разрешения конфликтов

<?php namespace Flute\Modules\Blog\Services; use Composer\Semver\Semver; class ConflictResolver { /** * Разрешение конфликтов версий */ public function resolveVersionConflicts(array $packages): array { $resolved = []; foreach ($packages as $package => $versions) { $resolved[$package] = $this->resolvePackageVersions($package, $versions); } return $resolved; } /** * Разрешение версий для пакета */ protected function resolvePackageVersions(string $package, array $versions): string { // Сортировка версий по предпочтительности usort($versions, function($a, $b) { return $this->compareVersions($b, $a); // Обратная сортировка }); // Выбор наиболее подходящей версии foreach ($versions as $version) { if ($this->isVersionCompatible($version)) { return $version; } } // Возврат последней версии как запасной вариант return end($versions); } /** * Сравнение версий */ protected function compareVersions(string $version1, string $version2): int { return version_compare($version1, $version2); } /** * Проверка совместимости версии */ protected function isVersionCompatible(string $version): bool { // Проверка на стабильность if (strpos($version, 'dev') !== false || strpos($version, 'alpha') !== false) { return false; } // Проверка на минимальную версию return version_compare($version, '1.0.0', '>='); } /** * Разрешение конфликтов зависимостей модулей */ public function resolveModuleConflicts(array $modules): array { $conflicts = []; $resolved = []; // Группировка модулей по зависимостям $dependencyGroups = $this->groupByDependencies($modules); foreach ($dependencyGroups as $dependency => $moduleVersions) { if (count($moduleVersions) > 1) { // Есть конфликт $resolvedVersion = $this->resolveModuleVersion($dependency, $moduleVersions); $conflicts[$dependency] = [ 'required_versions' => $moduleVersions, 'resolved_version' => $resolvedVersion ]; } $resolved[$dependency] = $moduleVersions[0]['version']; } return [ 'resolved' => $resolved, 'conflicts' => $conflicts ]; } /** * Группировка модулей по зависимостям */ protected function groupByDependencies(array $modules): array { $groups = []; foreach ($modules as $module) { $composerData = $this->getModuleComposerData($module); if (isset($composerData['require'])) { foreach ($composerData['require'] as $dependency => $version) { if (!isset($groups[$dependency])) { $groups[$dependency] = []; } $groups[$dependency][] = [ 'module' => $module, 'version' => $version ]; } } } return $groups; } /** * Разрешение версии модуля */ protected function resolveModuleVersion(string $dependency, array $moduleVersions): string { // Выбор наиболее строгой версии $versions = array_column($moduleVersions, 'version'); $highestVersion = $this->findHighestCompatibleVersion($versions); return $highestVersion; } /** * Поиск наиболее высокой совместимой версии */ protected function findHighestCompatibleVersion(array $versions): string { $parsedVersions = []; foreach ($versions as $version) { $parsedVersions[] = $this->parseVersionConstraint($version); } // Сортировка по верхней границе usort($parsedVersions, function($a, $b) { return version_compare($b['upper'], $a['upper']); }); return $parsedVersions[0]['original']; } /** * Парсинг ограничения версии */ protected function parseVersionConstraint(string $constraint): array { // Упрощенная логика парсинга // В реальном коде следует использовать Composer\Semver\VersionParser if (strpos($constraint, '^') === 0) { $baseVersion = substr($constraint, 1); $parts = explode('.', $baseVersion); $upperVersion = $parts[0] + 1 . '.0.0'; } elseif (strpos($constraint, '~') === 0) { $baseVersion = substr($constraint, 1); $parts = explode('.', $baseVersion); $upperVersion = $parts[0] . '.' . ($parts[1] + 1) . '.0'; } else { $upperVersion = $constraint; } return [ 'original' => $constraint, 'upper' => $upperVersion ]; } /** * Получение данных composer.json модуля */ protected function getModuleComposerData(string $modulePath): array { $composerFile = $modulePath . '/composer.json'; if (!file_exists($composerFile)) { return []; } $content = file_get_contents($composerFile); return json_decode($content, true) ?: []; } }

Тестирование зависимостей

Unit тесты

<?php namespace Tests\Modules\Blog\Services; use Flute\Modules\Blog\Services\DependencyManager; use Tests\TestCase; class DependencyManagerTest extends TestCase { protected DependencyManager $dependencyManager; protected function setUp(): void { parent::setUp(); $modulePath = app_path('Modules/Blog'); $this->dependencyManager = new DependencyManager($modulePath); } public function testCheckDependenciesWithValidRequirements(): void { // Мокаем composer.json с валидными зависимостями $this->mockComposerJson([ 'require' => [ 'php' => '^8.1', 'flute/cms' => '^1.0' ] ]); $result = $this->dependencyManager->checkDependencies(); $this->assertTrue($result['valid']); $this->assertEmpty($result['errors']); } public function testCheckDependenciesWithInvalidPhpVersion(): void { // Мокаем composer.json с неподходящей версией PHP $this->mockComposerJson([ 'require' => [ 'php' => '^9.0' // Текущая версия PHP ниже ] ]); $result = $this->dependencyManager->checkDependencies(); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertStringContains('PHP version', $result['errors'][0]); } public function testCheckDependenciesWithMissingPackage(): void { // Мокаем composer.json с неустановленным пакетом $this->mockComposerJson([ 'require' => [ 'nonexistent/package' => '^1.0' ] ]); $result = $this->dependencyManager->checkDependencies(); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertStringContains('not installed', $result['errors'][0]); } public function testCheckFluteDependencies(): void { $dependencies = [ 'flute' => '>=1.0.0', 'modules' => [ 'User' => '>=1.0.0' ], 'extensions' => ['pdo', 'mbstring'] ]; $errors = $this->dependencyManager->checkFluteDependencies($dependencies); // Предполагая что все зависимости удовлетворены $this->assertEmpty($errors); } protected function mockComposerJson(array $data): void { $composerFile = app_path('Modules/Blog/composer.json'); file_put_contents($composerFile, json_encode($data, JSON_PRETTY_PRINT)); } protected function tearDown(): void { parent::tearDown(); // Очистка мок файла $composerFile = app_path('Modules/Blog/composer.json'); if (file_exists($composerFile)) { unlink($composerFile); } } }

Лучшие практики

Управление зависимостями

  1. Указывайте точные версии — избегайте слишком широких диапазонов
  2. Тестируйте зависимости — проверяйте совместимость перед релизом
  3. Документируйте изменения — ведите changelog зависимостей
  4. Используйте стабильные версии — избегайте dev версий в продакшене

Структура composer.json

  1. Группируйте зависимости — разделяйте по назначению
  2. Добавляйте описания — документируйте назначение пакетов
  3. Указывайте лицензии — проверяйте совместимость лицензий
  4. Используйте алиасы — для удобства чтения

Безопасность

  1. Проверяйте пакеты — используйте только проверенные источники
  2. Мониторьте уязвимости — регулярно обновляйте зависимости
  3. Используйте lock файлы — фиксируйте точные версии
  4. Аудит зависимостей — проверяйте код на безопасность

Производительность

  1. Оптимизируйте автозагрузку — используйте оптимизированный автозагрузчик
  2. Кешируйте конфигурацию — избегайте повторного чтения файлов
  3. Ленивая загрузка — загружайте зависимости по необходимости
  4. Минифицируйте — уменьшайте размер зависимостей

Composer предоставляет мощную систему управления зависимостями для PHP проектов, обеспечивая надежность, безопасность и удобство разработки модулей Flute CMS.