diff --git a/CHANGELOG.md b/CHANGELOG.md index 7453330..f79e194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ ## 1.5.1 under development +- New #175: Add `yii-config-info` composer command (@vjik) - New #173: Allow to use option "config-plugin-file" in packages (@vjik) - Enh #172, #173: Refactoring: extract config settings reader to separate class (@vjik) +- Chg #175: Raise minimum Composer version to 2.3 (@vjik) +- Enh #175: Minor refactoring of internal classes `Options` and `ProcessHelper` (@vjik) ## 1.5.0 December 25, 2023 diff --git a/README.md b/README.md index 64f02cc..57ad6ed 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The package becomes a plugin holding both the code and its default configuration ## Requirements - PHP 8.0 or higher. -- Composer 2.0 or higher. +- Composer 2.3 or higher. ## Installation @@ -641,6 +641,15 @@ command: composer yii-config-rebuild ``` +### `yii-config-info` + +The `yii-config-rebuild` command displays application or package configuration details. + +```shell +composer yii-config-info +composer yii-config-info yiisoft/widget +``` + ## Testing ### Unit testing diff --git a/composer.json b/composer.json index ba5d6af..2399ca2 100644 --- a/composer.json +++ b/composer.json @@ -30,13 +30,14 @@ "require": { "php": "^8.0", "composer-plugin-api": "^2.0", + "symfony/console": "^5.4.11|^6.0.11|^7", "yiisoft/arrays": "^3.0", "yiisoft/strings": "^2.0", "yiisoft/var-dumper": "^1.1" }, "require-dev": { "ext-json": "*", - "composer/composer": "^2.0", + "composer/composer": "^2.3", "maglnet/composer-require-checker": "^4.4", "phpunit/phpunit": "^9.5", "rector/rector": "^1.0.0", diff --git a/src/Command/ConfigCommandProvider.php b/src/Command/ConfigCommandProvider.php index 425541f..98d7902 100644 --- a/src/Command/ConfigCommandProvider.php +++ b/src/Command/ConfigCommandProvider.php @@ -16,6 +16,7 @@ public function getCommands(): array return [ new CopyCommand(), new RebuildCommand(), + new InfoCommand(), ]; } } diff --git a/src/Command/CopyCommand.php b/src/Command/CopyCommand.php index 92f4a8b..543ae94 100644 --- a/src/Command/CopyCommand.php +++ b/src/Command/CopyCommand.php @@ -46,11 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var string[] $selectedFileNames */ $selectedFileNames = $input->getArgument('files'); - /** - * @psalm-suppress PossiblyNullArgument - * @psalm-suppress DeprecatedMethod - */ - $builder = new PackageFilesProcess($this->getComposer(), [$package]); + $builder = new PackageFilesProcess($this->requireComposer(), [$package]); $filesystem = new Filesystem(); $targetPath = $builder diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php new file mode 100644 index 0000000..8224bd6 --- /dev/null +++ b/src/Command/InfoCommand.php @@ -0,0 +1,165 @@ +setName('yii-config-info') + ->addArgument('package', InputArgument::OPTIONAL); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $composer = $this->requireComposer(); + + $packageName = $input->getArgument('package'); + if (is_string($packageName)) { + $package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*'); + if ($package === null) { + $io->error('Package "' . $packageName . '" not found.'); + return 1; + } + return $this->vendorPackage($composer, $package, $io); + } + + return $this->rootPackage($composer, $io); + } + + private function vendorPackage(Composer $composer, BasePackage $package, SymfonyStyle $io): int + { + $settings = ConfigSettings::forVendorPackage($composer, $package); + if (empty($settings->packageConfiguration())) { + $io->writeln(''); + $io->writeln('Configuration don\'t found in package "' . $package->getName() . '".'); + return 0; + } + + $io->title('Yii Config — Package "' . $package->getName() . '"'); + + $io->writeln('Source directory: ' . $settings->path() . '/' . $settings->options()->sourceDirectory()); + + $io->section('Configuration groups'); + $this->writeConfiguration($io, $settings->packageConfiguration()); + + return 0; + } + + private function rootPackage(Composer $composer, SymfonyStyle $io): int + { + $settings = ConfigSettings::forRootPackage($composer); + $options = $settings->options(); + $sourceDirectory = $settings->options()->sourceDirectory(); + $mergePlanFilePath = $settings->path() . '/' + . (empty($sourceDirectory) ? '' : ($sourceDirectory . '/')) + . $options->mergePlanFile(); + + $io->title('Yii Config — Root Configuration'); + + $io->section('Options'); + $io->table([], [ + [ + 'Build merge plan', + $options->buildMergePlan() ? 'yes' : 'no', + ], + [ + 'Merge plan file path', + file_exists($mergePlanFilePath) + ? '' . $mergePlanFilePath . '' + : '' . $mergePlanFilePath . ' (not exists)', + ], + [ + 'Package types', + empty($options->packageTypes()) ? 'not set' : implode(', ', $options->packageTypes()), + ], + [ + 'Source directory', + $settings->path() . '/' . $options->sourceDirectory(), + ], + [ + 'Vendor override layer packages', + empty($options->vendorOverrideLayerPackages()) + ? 'not set' + : implode(', ', $options->vendorOverrideLayerPackages()), + ], + ]); + + $io->section('Configuration groups'); + $this->writeConfiguration($io, $settings->packageConfiguration()); + + $io->section('Environments'); + $environmentsConfiguration = $settings->environmentsConfiguration(); + if (empty($environmentsConfiguration)) { + $io->writeln('not set'); + } else { + $isFirst = true; + foreach ($environmentsConfiguration as $environment => $groups) { + if ($isFirst) { + $isFirst = false; + } else { + $io->newLine(); + } + $io->write(' ' . $environment . ''); + if (empty($groups)) { + $io->writeln(' (empty)'); + } else { + $io->newLine(); + $this->writeConfiguration($io, $groups, offset: 2, addSeparateLine: false); + } + } + } + + return 0; + } + + /** + * @psalm-param array $configuration + */ + private function writeConfiguration( + SymfonyStyle $io, + array $configuration, + int $offset = 1, + bool $addSeparateLine = true, + ): void { + foreach ($configuration as $group => $values) { + $this->writeGroup($io, $group, $values, $offset); + if ($addSeparateLine) { + $io->newLine(); + } + } + } + + /** + * @param string|string[] $items + */ + private function writeGroup(SymfonyStyle $io, string $group, array|string $items, int $offset): void + { + $prefix = str_repeat(' ', $offset); + $items = (array) $items; + $io->write($prefix . '' . $group . ''); + if (empty($items)) { + $io->write(' (empty)'); + } else { + foreach ($items as $item) { + $io->newLine(); + $io->write($prefix . ' - ' . (Options::isVariable($item) ? '' . $item . '' : $item)); + } + } + $io->newLine(); + } +} diff --git a/src/Command/RebuildCommand.php b/src/Command/RebuildCommand.php index e43b662..e35ea4c 100644 --- a/src/Command/RebuildCommand.php +++ b/src/Command/RebuildCommand.php @@ -25,11 +25,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - /** - * @psalm-suppress PossiblyNullArgument - * @psalm-suppress DeprecatedMethod - */ - new MergePlanProcess($this->getComposer()); + new MergePlanProcess($this->requireComposer()); return 0; } } diff --git a/src/Composer/Options.php b/src/Composer/Options.php index 906a438..12bc967 100644 --- a/src/Composer/Options.php +++ b/src/Composer/Options.php @@ -5,6 +5,7 @@ namespace Yiisoft\Config\Composer; use function is_array; +use function is_string; use function str_replace; use function trim; @@ -23,8 +24,17 @@ final class Options private string $mergePlanFile = self::DEFAULT_MERGE_PLAN_FILE; private bool $buildMergePlan = true; + + /** + * @var string[] + */ private array $vendorOverrideLayerPackages = []; + private string $sourceDirectory = self::DEFAULT_CONFIG_DIRECTORY; + + /** + * @var string[] + */ private array $packageTypes = self::DEFAULT_PACKAGE_TYPES; public function __construct(array $extra) @@ -44,7 +54,10 @@ public function __construct(array $extra) } if (isset($options['vendor-override-layer'])) { - $this->vendorOverrideLayerPackages = (array) $options['vendor-override-layer']; + $this->vendorOverrideLayerPackages = array_filter( + (array) $options['vendor-override-layer'], + static fn(mixed $value): bool => is_string($value), + ); } if (isset($options['source-directory'])) { @@ -52,7 +65,10 @@ public function __construct(array $extra) } if (isset($options['package-types'])) { - $this->packageTypes = (array) $options['package-types']; + $this->packageTypes = array_filter( + (array) $options['package-types'], + static fn(mixed $value): bool => is_string($value), + ); } } @@ -81,6 +97,9 @@ public function buildMergePlan(): bool return $this->buildMergePlan; } + /** + * @return string[] + */ public function vendorOverrideLayerPackages(): array { return $this->vendorOverrideLayerPackages; @@ -91,6 +110,9 @@ public function sourceDirectory(): string return $this->sourceDirectory; } + /** + * @return string[] + */ public function packageTypes(): array { return $this->packageTypes; diff --git a/src/Composer/ProcessHelper.php b/src/Composer/ProcessHelper.php index 711e1ec..80e2e06 100644 --- a/src/Composer/ProcessHelper.php +++ b/src/Composer/ProcessHelper.php @@ -10,7 +10,6 @@ use Yiisoft\Config\ConfigPaths; use Yiisoft\Strings\WildcardPattern; -use function is_string; use function str_replace; /** @@ -202,10 +201,6 @@ private function getPackageRootDirectoryPath(PackageInterface $package): string private function isVendorOverridePackage(string $package): bool { foreach ($this->appConfigSettings->options()->vendorOverrideLayerPackages() as $pattern) { - if (!is_string($pattern)) { - continue; - } - if ($package === $pattern || (new WildcardPattern($pattern))->match($package)) { return true; } diff --git a/tests/Command/ConfigCommandProviderTest.php b/tests/Command/ConfigCommandProviderTest.php index da72b4c..6844f5c 100644 --- a/tests/Command/ConfigCommandProviderTest.php +++ b/tests/Command/ConfigCommandProviderTest.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command; use Yiisoft\Config\Command\ConfigCommandProvider; use Yiisoft\Config\Command\CopyCommand; +use Yiisoft\Config\Command\InfoCommand; use Yiisoft\Config\Command\RebuildCommand; final class ConfigCommandProviderTest extends TestCase @@ -17,12 +18,15 @@ public function testGetCommands(): void { $provider = new ConfigCommandProvider(); - $this->assertCount(2, $provider->getCommands()); + $this->assertCount(3, $provider->getCommands()); $this->assertInstanceOf(BaseCommand::class, $provider->getCommands()[0]); $this->assertInstanceOf(BaseCommand::class, $provider->getCommands()[1]); + $this->assertInstanceOf(BaseCommand::class, $provider->getCommands()[2]); $this->assertInstanceOf(Command::class, $provider->getCommands()[0]); $this->assertInstanceOf(Command::class, $provider->getCommands()[1]); + $this->assertInstanceOf(Command::class, $provider->getCommands()[2]); $this->assertEquals($provider->getCommands()[0], new CopyCommand()); $this->assertEquals($provider->getCommands()[1], new RebuildCommand()); + $this->assertEquals($provider->getCommands()[2], new InfoCommand()); } } diff --git a/tests/Integration/InfoCommandTest/InfoCommandTest.php b/tests/Integration/InfoCommandTest/InfoCommandTest.php new file mode 100644 index 0000000..8f526e6 --- /dev/null +++ b/tests/Integration/InfoCommandTest/InfoCommandTest.php @@ -0,0 +1,119 @@ +runInfoCommand(); + + $this->assertStringContainsString('Yii Config — Root Configuration', $output); + $this->assertMatchesRegularExpression('~Build merge plan\s*yes~', $output); + $this->assertMatchesRegularExpression( + '~Merge plan file path\s*' + . preg_quote($rootPath . '/config/' . Options::DEFAULT_MERGE_PLAN_FILE, '~') + . '~', + $output + ); + $this->assertMatchesRegularExpression('~Package types\s*library, composer-plugin~', $output); + $this->assertMatchesRegularExpression( + '~Source directory\s*' . preg_quote($rootPath . '/config', '~') . '~', + $output + ); + $this->assertMatchesRegularExpression('~Vendor override layer packages\s*not set~', $output); + $this->assertStringContainsString('Configuration groups', $output); + $this->assertStringContainsString('- params/*.php', $output); + $this->assertStringContainsString('widgets (empty)', $output); + $this->assertStringContainsString('Environments', $output); + $this->assertStringContainsString('environment/params/*.php', $output); + $this->assertStringContainsString('development (empty)', $output); + } + + public function testVendorPackage(): void + { + [$rootPath, $output] = $this->runInfoCommand('test/a'); + + $this->assertStringContainsString('Yii Config — Package "test/a"', $output); + $this->assertStringContainsString( + 'Source directory: ' . $rootPath . DIRECTORY_SEPARATOR . 'vendor/test/a/config', + $output + ); + $this->assertStringContainsString('Configuration groups', $output); + $this->assertStringContainsString('- params.php', $output); + $this->assertStringContainsString('- web.php', $output); + } + + public function testPackageNotFound() + { + [, $output] = $this->runInfoCommand('unknown/test'); + + $this->assertStringContainsString('Package "unknown/test" not found.', $output); + } + + public function testPackageWithoutConfiguration() + { + [, $output] = $this->runInfoCommand('test/b'); + + $this->assertStringContainsString('Configuration don\'t found in package "test/b".', $output); + } + + public function testWithoutEnvironments() + { + [, $output] = $this->runInfoCommand(withEnvironments: false); + + $this->assertMatchesRegularExpression('~Environments(\s|-)+not set~', $output); + } + + private function runInfoCommand(?string $package = null, bool $withEnvironments = true): array + { + $rootPath = __DIR__; + $packages = [ + 'test/a' => __DIR__ . '/packages/a', + 'test/b' => __DIR__ . '/packages/b', + ]; + $extra = [ + 'config-plugin-options' => [ + 'source-directory' => 'config', + ], + 'config-plugin' => [ + 'params' => 'params/*.php', + 'widgets' => [], + ], + 'config-plugin-environments' => $withEnvironments + ? [ + 'environment' => [ + 'params' => 'environment/params/*.php', + ], + 'development' => [], + ] + : [], + ]; + + $this->runComposerUpdate( + rootPath: $rootPath, + packages: $packages, + extra: $extra, + configDirectory: 'config', + ); + + $arguments = ['command' => 'yii-config-info']; + if ($package !== null) { + $arguments['package'] = $package; + } + $output = $this->runComposerCommand( + $arguments, + rootPath: $rootPath, + packages: $packages, + extra: $extra, + configDirectory: 'config', + ); + + return [$rootPath, $output]; + } +} diff --git a/tests/Integration/InfoCommandTest/config/environment/params/a.php b/tests/Integration/InfoCommandTest/config/environment/params/a.php new file mode 100644 index 0000000..0dae23d --- /dev/null +++ b/tests/Integration/InfoCommandTest/config/environment/params/a.php @@ -0,0 +1,5 @@ +