Skip to Content
ModulesComposer Dependency Management

Composer Dependency Management

Flute CMS uses Composer for dependencies of the entire project. Modules do not necessarily need to have a composer.json: dependencies are mainly specified in the root composer.json. If a module has its own composer.json, ModuleManager::runComposerInstall() will execute composer install from the project root (only if the file exists), but no special magic with extra.flute is applied.

What is actually used:

  • The extra.flute field is not processed by the core: dependencies and metadata are taken from module.json
  • PSR-4 autoloading for modules is configured via the root composer ("Flute\\": "app/"), and module composer.json files are not automatically merged into autoload
  • If a module needs third-party packages, add them to the root composer.json or keep a separate composer.json in the module and run composer install manually/via ModuleManager::runComposerInstall()

Module composer.json Structure

Basic Structure

{ "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 }

Dependency Management

Dependency Management Service

<?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(); } /** * Load data from 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(); } /** * Check dependencies */ public function checkDependencies(): array { $errors = []; $warnings = []; // Check PHP version if (isset($this->composerData['require']['php'])) { $phpConstraint = $this->composerData['require']['php']; if (!$this->checkPhpVersion($phpConstraint)) { $errors[] = "PHP version {$phpConstraint} required, current: " . PHP_VERSION; } } // Check Composer dependencies 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"; } } } } // Check Flute specific dependencies 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 ]; } /** * Check PHP version */ protected function checkPhpVersion(string $constraint): bool { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($constraint); return $constraint->matches($versionParser->parseConstraints(PHP_VERSION)); } /** * Check package dependency */ 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 ]; } /** * Get installed packages */ 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; } /** * Check Flute specific dependencies */ protected function checkFluteDependencies(array $dependencies): array { $errors = []; // Check Flute version if (isset($dependencies['flute'])) { if (!$this->checkFluteVersion($dependencies['flute'])) { $errors[] = "Flute CMS version {$dependencies['flute']} required"; } } // Check modules if (isset($dependencies['modules'])) { foreach ($dependencies['modules'] as $module => $version) { if (!$this->checkModuleDependency($module, $version)) { $errors[] = "Module {$module} version {$version} required"; } } } // Check PHP extensions if (isset($dependencies['extensions'])) { foreach ($dependencies['extensions'] as $extension) { if (!extension_loaded($extension)) { $errors[] = "PHP extension {$extension} is required"; } } } return $errors; } /** * Check Flute version */ 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); } /** * Check module dependency */ 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); } /** * Install dependencies */ 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; } } /** * Update dependencies */ 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; } } /** * Run Composer command */ 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 minutes $process->run(); if (!$process->isSuccessful()) { throw new \Exception("Composer command failed: " . $process->getErrorOutput()); } } /** * Generate 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); } }

Note: the provided manager is an example. In a real project, dependency checks are done by ModuleDependencies, and Composer packages are installed from the root.

Installing Modules via Composer

Custom Installer

<?php namespace Flute\Modules\Blog; use Composer\Script\Event; use Flute\Modules\Blog\Services\DependencyManager; class Installer { /** * Post-install script */ 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; } } /** * Post-update script */ 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; } } /** * Run installation */ protected static function runInstallation($composer, $io): void { $modulePath = dirname(__DIR__, 2); // Path to module $dependencyManager = new DependencyManager($modulePath); // Check dependencies $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>'); } } // Create necessary directories self::createDirectories($modulePath, $io); // Copy config files self::copyConfigFiles($modulePath, $io); // Run migrations self::runMigrations($modulePath, $io); // Publish assets self::publishAssets($modulePath, $io); // Register in system self::registerModule($modulePath, $io); } /** * Create directories */ 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}"); } } } /** * Copy config files */ 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}"); } } } /** * Run migrations */ protected static function runMigrations(string $modulePath, $io): void { $io->write('Running migrations...'); // Run migrations via Flute $migrationFiles = glob($modulePath . '/database/migrations/*.php'); foreach ($migrationFiles as $migrationFile) { $migrationClass = basename($migrationFile, '.php'); $io->write("Running migration: {$migrationClass}"); // Logic to run migration should be here // In real code this would integrate with Flute migration system } } /** * Publish assets */ 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'); } /** * Register module */ 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'); } /** * Run update */ protected static function runUpdate($composer, $io): void { $modulePath = dirname(__DIR__, 2); $dependencyManager = new DependencyManager($modulePath); // Check dependencies $result = $dependencyManager->checkDependencies(); if (!$result['valid']) { $io->writeError('<error>Cannot update: dependencies not satisfied</error>'); return; } // Update assets self::publishAssets($modulePath, $io); // Run update migrations self::runUpdateMigrations($modulePath, $io); } /** * Run update migrations */ protected static function runUpdateMigrations(string $modulePath, $io): void { $io->write('Running update migrations...'); // Update migration logic // Check module version and run corresponding migrations } }

Dependency Conflict Resolution

Conflict Resolution Service

<?php namespace Flute\Modules\Blog\Services; use Composer\Semver\Semver; class ConflictResolver { /** * Resolve version conflicts */ public function resolveVersionConflicts(array $packages): array { $resolved = []; foreach ($packages as $package => $versions) { $resolved[$package] = $this->resolvePackageVersions($package, $versions); } return $resolved; } /** * Resolve versions for package */ protected function resolvePackageVersions(string $package, array $versions): string { // Sort versions by preference usort($versions, function($a, $b) { return $this->compareVersions($b, $a); // Reverse sort }); // Choose most suitable version foreach ($versions as $version) { if ($this->isVersionCompatible($version)) { return $version; } } // Return latest version as fallback return end($versions); } /** * Compare versions */ protected function compareVersions(string $version1, string $version2): int { return version_compare($version1, $version2); } /** * Check version compatibility */ protected function isVersionCompatible(string $version): bool { // Check stability if (strpos($version, 'dev') !== false || strpos($version, 'alpha') !== false) { return false; } // Check minimum version return version_compare($version, '1.0.0', '>='); } /** * Resolve module dependency conflicts */ public function resolveModuleConflicts(array $modules): array { $conflicts = []; $resolved = []; // Group modules by dependencies $dependencyGroups = $this->groupByDependencies($modules); foreach ($dependencyGroups as $dependency => $moduleVersions) { if (count($moduleVersions) > 1) { // Conflict exists $resolvedVersion = $this->resolveModuleVersion($dependency, $moduleVersions); $conflicts[$dependency] = [ 'required_versions' => $moduleVersions, 'resolved_version' => $resolvedVersion ]; } $resolved[$dependency] = $moduleVersions[0]['version']; } return [ 'resolved' => $resolved, 'conflicts' => $conflicts ]; } /** * Group modules by dependencies */ 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; } /** * Resolve module version */ protected function resolveModuleVersion(string $dependency, array $moduleVersions): string { // Choose strictest version $versions = array_column($moduleVersions, 'version'); $highestVersion = $this->findHighestCompatibleVersion($versions); return $highestVersion; } /** * Find highest compatible version */ protected function findHighestCompatibleVersion(array $versions): string { $parsedVersions = []; foreach ($versions as $version) { $parsedVersions[] = $this->parseVersionConstraint($version); } // Sort by upper bound usort($parsedVersions, function($a, $b) { return version_compare($b['upper'], $a['upper']); }); return $parsedVersions[0]['original']; } /** * Parse version constraint */ protected function parseVersionConstraint(string $constraint): array { // Simplified parsing logic // In real code use 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 ]; } /** * Get module composer.json data */ 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) ?: []; } }

Testing Dependencies

Unit Tests

<?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 { // Mock composer.json with valid dependencies $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 { // Mock composer.json with incompatible PHP version $this->mockComposerJson([ 'require' => [ 'php' => '^9.0' // Current PHP version is lower ] ]); $result = $this->dependencyManager->checkDependencies(); $this->assertFalse($result['valid']); $this->assertNotEmpty($result['errors']); $this->assertStringContains('PHP version', $result['errors'][0]); } public function testCheckDependenciesWithMissingPackage(): void { // Mock composer.json with missing package $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); // Assuming all dependencies are satisfied $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(); // Clean up mock file $composerFile = app_path('Modules/Blog/composer.json'); if (file_exists($composerFile)) { unlink($composerFile); } } }

Best Practices

Dependency Management

  1. Specify exact versions — avoid ranges that are too broad
  2. Test dependencies — check compatibility before release
  3. Document changes — maintain a dependency changelog
  4. Use stable versions — avoid dev versions in production

composer.json Structure

  1. Group dependencies — separate by purpose
  2. Add descriptions — document package purpose
  3. Specify licenses — check license compatibility
  4. Use aliases — for readability

Security

  1. Check packages — use only trusted sources
  2. Monitor vulnerabilities — regularly update dependencies
  3. Use lock files — fix exact versions
  4. Audit dependencies — check code for security issues

Performance

  1. Optimize autoloading — use optimized autoloader
  2. Cache configuration — avoid rereading files
  3. Lazy loading — load dependencies as needed
  4. Minify — reduce dependency size

Composer provides a powerful dependency management system for PHP projects, ensuring reliability, security, and convenience for Flute CMS module development.