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.flutefield is not processed by the core: dependencies and metadata are taken frommodule.json - PSR-4 autoloading for modules is configured via the root composer (
"Flute\\": "app/"), and modulecomposer.jsonfiles are not automatically merged into autoload - If a module needs third-party packages, add them to the root
composer.jsonor keep a separatecomposer.jsonin the module and runcomposer installmanually/viaModuleManager::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
- Specify exact versions — avoid ranges that are too broad
- Test dependencies — check compatibility before release
- Document changes — maintain a dependency changelog
- Use stable versions — avoid dev versions in production
composer.json Structure
- Group dependencies — separate by purpose
- Add descriptions — document package purpose
- Specify licenses — check license compatibility
- Use aliases — for readability
Security
- Check packages — use only trusted sources
- Monitor vulnerabilities — regularly update dependencies
- Use lock files — fix exact versions
- Audit dependencies — check code for security issues
Performance
- Optimize autoloading — use optimized autoloader
- Cache configuration — avoid rereading files
- Lazy loading — load dependencies as needed
- Minify — reduce dependency size
Composer provides a powerful dependency management system for PHP projects, ensuring reliability, security, and convenience for Flute CMS module development.