From bf980d25012e01a7286014634789003536b511a5 Mon Sep 17 00:00:00 2001 From: vinchan Date: Thu, 5 Feb 2026 14:49:28 +0800 Subject: [PATCH 1/6] feat(*):Refactor ComposerIntegrationHandler to streamline installer creation and execution --- .../ComposerIntegrationHandler.php | 81 ++++++++++++------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 6a13b6b3..a1855fed 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -9,6 +9,7 @@ use Composer\Installer; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\ExtensionName; use Php\Pie\Platform; use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; @@ -22,8 +23,7 @@ public function __construct( private readonly ContainerInterface $container, private readonly QuieterConsoleIO $arrayCollectionIo, private readonly VendorCleanup $vendorCleanup, - ) { - } + ) {} public function runInstall( Package $package, @@ -53,7 +53,6 @@ public function runInstall( } // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file - $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); $originalPieJsonContent = $pieJsonEditor->addRequire( $requestedPackageAndVersion->package, @@ -68,33 +67,15 @@ public function runInstall( $composer->getRepositoryManager()->getLocalRepository()->removePackage($pkg); } - $composerInstaller = PieComposerInstaller::createWithPhpBinary( - $targetPlatform->phpBinaryPath, + $composerInstaller = $this->createConfiguredInstaller( + $targetPlatform, $package->extensionName(), - $this->arrayCollectionIo, $composer, + $requestedPackageAndVersion->package, + $forceInstallPackageVersion, ); - $composerInstaller - ->setAllowedTypes(['php-ext', 'php-ext-zend']) - ->setInstall(true) - ->setIgnoredTypes([]) - ->setDryRun(false) - ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion)) - ->setDownloadOnly(false); - - if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { - $composerInstaller->setUpdate(true); - $composerInstaller->setUpdateAllowList([$requestedPackageAndVersion->package]); - } - - $resultCode = $composerInstaller->run(); - - if ($resultCode !== Installer::ERROR_NONE) { - // Revert composer.json change - $pieJsonEditor->revert($originalPieJsonContent); - throw ComposerRunFailed::fromExitCode($resultCode); - } + $this->executeComposerInstaller($composerInstaller, $pieJsonEditor, $originalPieJsonContent); if (! $runCleanup) { return; @@ -110,19 +91,42 @@ public function runUninstall( RequestedPackageAndVersion $requestedPackageAndVersionToRemove, ): void { // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file - $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); $originalPieJsonContent = $pieJsonEditor->removeRequire($requestedPackageAndVersionToRemove->package); // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); + $composerInstaller = $this->createConfiguredInstaller( + $targetPlatform, + $packageToRemove->extensionName(), + $composer, + $requestedPackageAndVersionToRemove->package, + false, + ); + + $this->executeComposerInstaller($composerInstaller, $pieJsonEditor, $originalPieJsonContent); + } + + /** + * Creates and configures a PieComposerInstaller instance with common settings. + */ + private function createConfiguredInstaller( + TargetPlatform $targetPlatform, + ExtensionName $extensionName, + Composer $composer, + string $packageName, + bool $forceInstallPackageVersion, + ): PieComposerInstaller { + $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); + $composerInstaller = PieComposerInstaller::createWithPhpBinary( $targetPlatform->phpBinaryPath, - $packageToRemove->extensionName(), + $extensionName, $this->arrayCollectionIo, $composer, ); + $composerInstaller ->setAllowedTypes(['php-ext', 'php-ext-zend']) ->setInstall(true) @@ -130,11 +134,30 @@ public function runUninstall( ->setDryRun(false) ->setDownloadOnly(false); + if ($forceInstallPackageVersion) { + $composerInstaller->setPlatformRequirementFilter( + PlatformRequirementFilterFactory::fromBoolOrList(true), + ); + } + if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { $composerInstaller->setUpdate(true); - $composerInstaller->setUpdateAllowList([$requestedPackageAndVersionToRemove->package]); + $composerInstaller->setUpdateAllowList([$packageName]); } + return $composerInstaller; + } + + /** + * Executes the Composer installer and handles errors with automatic revert. + * + * @throws ComposerRunFailed + */ + private function executeComposerInstaller( + PieComposerInstaller $composerInstaller, + PieJsonEditor $pieJsonEditor, + string $originalPieJsonContent, + ): void { $resultCode = $composerInstaller->run(); if ($resultCode !== Installer::ERROR_NONE) { From 60ae6247482b4a21f7555e8f9c8bef68579c3d20 Mon Sep 17 00:00:00 2001 From: vinchan Date: Fri, 6 Feb 2026 12:09:24 +0800 Subject: [PATCH 2/6] feat(*):support installing multiple extensions --- features/install-multiple-extensions.feature | 18 ++ src/Command/CommandHelper.php | 73 +++++-- src/Command/InstallCommand.php | 204 +++++++++++++++---- test/unit/Command/CommandHelperTest.php | 77 ++++++- 4 files changed, 308 insertions(+), 64 deletions(-) create mode 100644 features/install-multiple-extensions.feature diff --git a/features/install-multiple-extensions.feature b/features/install-multiple-extensions.feature new file mode 100644 index 00000000..f9bd778b --- /dev/null +++ b/features/install-multiple-extensions.feature @@ -0,0 +1,18 @@ +Feature: Multiple extensions can be installed with PIE + + # pie install + Example: Multiple extensions can be installed at once + When I run a command to install multiple extensions + Then all extensions should have been installed + And the output should show the installation summary + + # pie install --skip-enable-extension + Example: Multiple extensions can be installed without enabling + When I run a command to install multiple extensions without enabling them + Then all extensions should have been installed + And the extensions should not be enabled + + Example: Installation continues when one extension fails + When I run a command to install multiple extensions where one fails + Then the successful extensions should have been installed + And the output should show which extensions failed diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 54b834a9..a69e15fc 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,9 +64,7 @@ final class CommandHelper private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; - private function __construct() - { - } + private function __construct() {} public static function configurePhpConfigOptions(Command $command): void { @@ -202,7 +200,8 @@ public static function determineTargetPlatformFromInputs(InputInterface $input, $phpBinaryPath = PhpBinaryPath::fromPhpBinaryPath($withPhpPath); } - $makeParallelJobs = null; /** `null` means {@see TargetPlatform} will try to auto-detect */ + $makeParallelJobs = null; + /** `null` means {@see TargetPlatform} will try to auto-detect */ if ($input->hasOption(self::OPTION_MAKE_PARALLEL_JOBS)) { $makeParallelJobsOptions = (int) $input->getOption(self::OPTION_MAKE_PARALLEL_JOBS); if ($makeParallelJobsOptions > 0) { @@ -276,33 +275,65 @@ public static function determinePhpizePathFromInputs(InputInterface $input): Php return null; } - public static function requestedNameAndVersionPair(InputInterface $input): RequestedPackageAndVersion + /** @return list */ + public static function requestedNameAndVersionPairs(InputInterface $input): array { - $requestedPackageString = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); + $requestedPackages = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); + + if (! is_array($requestedPackages)) { + // 兼容单个字符串的情况(向后兼容) + if (is_string($requestedPackages) && $requestedPackages !== '') { + $requestedPackages = [$requestedPackages]; + } else { + throw new InvalidArgumentException('No package was requested for installation'); + } + } - if (! is_string($requestedPackageString) || $requestedPackageString === '') { + if (count($requestedPackages) === 0) { throw new InvalidArgumentException('No package was requested for installation'); } - $nameAndVersionPairs = (new VersionParser()) - ->parseNameVersionPairs([$requestedPackageString]); - $requestedNameAndVersionPair = reset($nameAndVersionPairs); + $versionParser = new VersionParser(); + $results = []; - if (! is_array($requestedNameAndVersionPair)) { - throw new InvalidArgumentException('Failed to parse the name/version pair'); - } + foreach ($requestedPackages as $requestedPackageString) { + if (! is_string($requestedPackageString) || $requestedPackageString === '') { + continue; + } + + $nameAndVersionPairs = $versionParser->parseNameVersionPairs([$requestedPackageString]); + $requestedNameAndVersionPair = reset($nameAndVersionPairs); - if (! array_key_exists('version', $requestedNameAndVersionPair)) { - $requestedNameAndVersionPair['version'] = null; + if (! is_array($requestedNameAndVersionPair)) { + throw new InvalidArgumentException('Failed to parse the name/version pair: ' . $requestedPackageString); + } + + if (! array_key_exists('version', $requestedNameAndVersionPair)) { + $requestedNameAndVersionPair['version'] = null; + } + + Assert::stringNotEmpty($requestedNameAndVersionPair['name']); + Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); + + $results[] = new RequestedPackageAndVersion( + $requestedNameAndVersionPair['name'], + $requestedNameAndVersionPair['version'], + ); } - Assert::stringNotEmpty($requestedNameAndVersionPair['name']); - Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); + return $results; + } - return new RequestedPackageAndVersion( - $requestedNameAndVersionPair['name'], - $requestedNameAndVersionPair['version'], - ); + public static function requestedNameAndVersionPair(InputInterface $input): RequestedPackageAndVersion + { + $pairs = self::requestedNameAndVersionPairs($input); + + if (count($pairs) === 0) { + throw new InvalidArgumentException('No package was requested for installation'); + } + + // 如果传入多个,只返回第一个(向后兼容单包逻辑) + return $pairs[0]; } public static function bindConfigureOptionsFromPackage(Command $command, Package $package, InputInterface $input): void diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 33e93d31..bd5e04a7 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -13,6 +13,7 @@ use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\InvalidPackageName; +use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform\TargetPlatform; @@ -21,6 +22,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -48,12 +50,19 @@ public function configure(): void { parent::configure(); - CommandHelper::configureDownloadBuildInstallOptions($this); + $this->addArgument( + CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION, + InputArgument::IS_ARRAY | InputArgument::OPTIONAL, + 'The PIE package name(s) and version constraint(s) to install. Can specify multiple packages separated by space, e.g., xdebug/xdebug redis/redis:^5.0', + ); + + CommandHelper::configureDownloadBuildInstallOptions($this, false); } public function execute(InputInterface $input, OutputInterface $output): int { - if (! $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION)) { + $requestedPackages = $input->getArgument(CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION); + if (! is_array($requestedPackages) || count($requestedPackages) === 0) { return ($this->invokeSubCommand)( $this, ['command' => 'install-extensions-for-project'], @@ -66,8 +75,10 @@ public function execute(InputInterface $input, OutputInterface $output): int } $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + + // 解析所有待安装的包 try { - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $requestedPackagesList = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { return CommandHelper::handlePackageNotFound( $invalidPackageName, @@ -90,6 +101,122 @@ public function execute(InputInterface $input, OutputInterface $output): int ); } + $totalPackages = count($requestedPackagesList); + + // 如果只有一个包,使用原有的详细输出模式 + if ($totalPackages === 1) { + return $this->installSinglePackage( + $requestedPackagesList[0], + $targetPlatform, + $forceInstallPackageVersion, + $input, + true, // verbose mode + ); + } + + // 多包安装模式 + $this->io->write(sprintf( + 'Installing %d extensions...', + $totalPackages, + )); + $this->io->write(''); + + $successCount = 0; + $failedPackages = []; + + foreach ($requestedPackagesList as $index => $requestedNameAndVersion) { + $packageNumber = $index + 1; + $this->io->write(sprintf( + '[%d/%d] Processing %s...', + $packageNumber, + $totalPackages, + $requestedNameAndVersion->package, + )); + + try { + $installResult = $this->installSinglePackage( + $requestedNameAndVersion, + $targetPlatform, + $forceInstallPackageVersion, + $input, + false, // quiet mode + ); + + if ($installResult === Command::SUCCESS) { + $successCount++; + $this->io->write(sprintf( + '✓ Successfully installed %s', + $requestedNameAndVersion->package, + )); + } else { + $failedPackages[] = $requestedNameAndVersion->package; + $this->io->writeError(sprintf( + '✗ Failed to install %s', + $requestedNameAndVersion->package, + )); + } + } catch (UnableToResolveRequirement $unableToResolveRequirement) { + $failedPackages[] = $requestedNameAndVersion->package; + $this->io->writeError(sprintf( + '✗ Could not resolve %s: %s', + $requestedNameAndVersion->package, + $unableToResolveRequirement->getMessage(), + )); + } catch (BundledPhpExtensionRefusal $bundledPhpExtensionRefusal) { + $failedPackages[] = $requestedNameAndVersion->package; + $this->io->writeError(sprintf( + '✗ Skipped %s: %s', + $requestedNameAndVersion->package, + $bundledPhpExtensionRefusal->getMessage(), + )); + } catch (ComposerRunFailed $composerRunFailed) { + $failedPackages[] = $requestedNameAndVersion->package; + $this->io->writeError(sprintf( + '✗ Installation failed for %s: %s', + $requestedNameAndVersion->package, + $composerRunFailed->getMessage(), + )); + } + + $this->io->write(''); + } + + // 输出总结 + $this->io->write('====================================='); + $this->io->write(sprintf( + 'Installation Summary: %d succeeded, %d failed out of %d total', + $successCount, + count($failedPackages), + $totalPackages, + )); + + if (count($failedPackages) > 0) { + $this->io->write('Failed packages:'); + foreach ($failedPackages as $failedPackage) { + $this->io->write(sprintf(' - %s', $failedPackage)); + } + + return Command::FAILURE; + } + + return Command::SUCCESS; + } + + /** + * 安装单个扩展包 + * + * @throws UnableToResolveRequirement + * @throws BundledPhpExtensionRefusal + * @throws ComposerRunFailed + * @throws InvalidPackageName + */ + private function installSinglePackage( + RequestedPackageAndVersion $requestedNameAndVersion, + TargetPlatform $targetPlatform, + bool $forceInstallPackageVersion, + InputInterface $input, + bool $verboseOutput = true, + ): int { $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( @@ -97,38 +224,37 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform, $requestedNameAndVersion, PieOperation::Resolve, - [], // Configure options are not needed for resolve only + [], null, - false, // setting up INI not needed for resolve step + false, ), ); - try { - $package = ($this->dependencyResolver)( - $composer, - $targetPlatform, - $requestedNameAndVersion, - $forceInstallPackageVersion, - ); - } catch (UnableToResolveRequirement $unableToResolveRequirement) { - return CommandHelper::handlePackageNotFound( - $unableToResolveRequirement, - $this->findMatchingPackages, - $this->io, - $targetPlatform, - $this->container, - ); - } catch (BundledPhpExtensionRefusal $bundledPhpExtensionRefusal) { - $this->io->writeError(''); - $this->io->writeError('' . $bundledPhpExtensionRefusal->getMessage() . ''); + $package = ($this->dependencyResolver)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); - return self::INVALID; + if ($verboseOutput) { + $this->io->write(sprintf( + 'Found package: %s which provides %s', + $package->prettyNameAndVersion(), + $package->extensionName()->nameWithExtPrefix(), + )); + } else { + $this->io->write( + sprintf( + ' Found package: %s which provides %s', + $package->prettyNameAndVersion(), + $package->extensionName()->nameWithExtPrefix(), + ), + verbosity: IOInterface::VERBOSE, + ); } - $this->io->write(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); - - // Now we know what package we have, we can validate the configure options for the command and re-create the - // Composer instance with the populated configure options + // 验证配置选项 CommandHelper::bindConfigureOptionsFromPackage($this, $package, $input); $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($package, $input); @@ -145,20 +271,14 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - try { - $this->composerIntegrationHandler->runInstall( - $package, - $composer, - $targetPlatform, - $requestedNameAndVersion, - $forceInstallPackageVersion, - true, - ); - } catch (ComposerRunFailed $composerRunFailed) { - $this->io->writeError('' . $composerRunFailed->getMessage() . ''); - - return $composerRunFailed->getCode(); - } + $this->composerIntegrationHandler->runInstall( + $package, + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + true, + ); return Command::SUCCESS; } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 04c1f1ee..c27fff3c 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -49,7 +49,7 @@ public static function validPackageAndVersions(): array ]; return array_combine( - array_map(static fn (array $data) => $data[0], $packages), + array_map(static fn(array $data) => $data[0], $packages), $packages, ); } @@ -88,6 +88,81 @@ public function testInvalidRequestedNameAndVersionPairThrowsExceptionWhenNoPacka CommandHelper::requestedNameAndVersionPair($input); } + public function testRequestedNameAndVersionPairsWithMultiplePackages(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn(['php/ext1', 'php/ext2:^1.0', 'php/ext3:@alpha']); + + $result = CommandHelper::requestedNameAndVersionPairs($input); + + self::assertCount(3, $result); + self::assertEquals(new RequestedPackageAndVersion('php/ext1', null), $result[0]); + self::assertEquals(new RequestedPackageAndVersion('php/ext2', '^1.0'), $result[1]); + self::assertEquals(new RequestedPackageAndVersion('php/ext3', '@alpha'), $result[2]); + } + + public function testRequestedNameAndVersionPairsWithSinglePackage(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn(['php/ext1:^2.0']); + + $result = CommandHelper::requestedNameAndVersionPairs($input); + + self::assertCount(1, $result); + self::assertEquals(new RequestedPackageAndVersion('php/ext1', '^2.0'), $result[0]); + } + + public function testRequestedNameAndVersionPairsBackwardsCompatibleWithString(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn('php/ext1:^1.0'); + + $result = CommandHelper::requestedNameAndVersionPairs($input); + + self::assertCount(1, $result); + self::assertEquals(new RequestedPackageAndVersion('php/ext1', '^1.0'), $result[0]); + } + + public function testRequestedNameAndVersionPairsThrowsExceptionWhenEmpty(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn([]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No package was requested for installation'); + CommandHelper::requestedNameAndVersionPairs($input); + } + + public function testRequestedNameAndVersionPairUsesFirstFromMultiple(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn(['php/ext1', 'php/ext2']); + + $result = CommandHelper::requestedNameAndVersionPair($input); + + self::assertEquals(new RequestedPackageAndVersion('php/ext1', null), $result); + } + public function testBindingConfigurationOptionsFromPackage(): void { self::markTestIncomplete(__METHOD__); From 337a911ac604f476050e5f964a31ec6b76ee7998 Mon Sep 17 00:00:00 2001 From: vinchan Date: Fri, 6 Feb 2026 13:41:50 +0800 Subject: [PATCH 3/6] fix(*):optimizing PHPUnit error issues --- test/integration/Command/InstallCommandTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index dd39e533..db0151cb 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -58,13 +58,13 @@ public static function phpPathProvider(): array '/usr/bin/php-config8.0', '/usr/bin/php-config7.4', ], - static fn (string $phpConfigPath) => file_exists($phpConfigPath) + static fn(string $phpConfigPath) => file_exists($phpConfigPath) && is_executable($phpConfigPath), ); return array_combine( $possiblePhpConfigPaths, - array_map(static fn (string $phpConfigPath) => [$phpConfigPath], $possiblePhpConfigPaths), + array_map(static fn(string $phpConfigPath) => [$phpConfigPath], $possiblePhpConfigPaths), ); } @@ -77,7 +77,7 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin $this->commandTester->execute( [ - 'requested-package-and-version' => self::TEST_PACKAGE, + 'requested-package-and-version' => [self::TEST_PACKAGE], '--with-php-config' => $phpConfigPath, '--skip-enable-extension' => true, ], @@ -114,7 +114,7 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin public function testInstallCommandWillInstallCompatibleExtensionWindows(): void { $this->commandTester->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE, + 'requested-package-and-version' => [self::TEST_PACKAGE], '--skip-enable-extension' => true, ]); From abe0c469970e828afec26dcdf69aef5b64ba3215 Mon Sep 17 00:00:00 2001 From: vinchan Date: Fri, 6 Feb 2026 13:46:15 +0800 Subject: [PATCH 4/6] style(*):code style --- src/Command/CommandHelper.php | 10 ++++++---- src/Command/InstallCommand.php | 2 ++ src/ComposerIntegration/ComposerIntegrationHandler.php | 3 ++- test/integration/Command/InstallCommandTest.php | 4 ++-- test/unit/Command/CommandHelperTest.php | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index a69e15fc..c1e04d32 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,7 +64,9 @@ final class CommandHelper private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; - private function __construct() {} + private function __construct() + { + } public static function configurePhpConfigOptions(Command $command): void { @@ -282,11 +284,11 @@ public static function requestedNameAndVersionPairs(InputInterface $input): arra if (! is_array($requestedPackages)) { // 兼容单个字符串的情况(向后兼容) - if (is_string($requestedPackages) && $requestedPackages !== '') { - $requestedPackages = [$requestedPackages]; - } else { + if (! is_string($requestedPackages) || $requestedPackages === '') { throw new InvalidArgumentException('No package was requested for installation'); } + + $requestedPackages = [$requestedPackages]; } if (count($requestedPackages) === 0) { diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index bd5e04a7..fffa902d 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -26,6 +26,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function count; +use function is_array; use function sprintf; #[AsCommand( diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index a1855fed..8c620e80 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -23,7 +23,8 @@ public function __construct( private readonly ContainerInterface $container, private readonly QuieterConsoleIO $arrayCollectionIo, private readonly VendorCleanup $vendorCleanup, - ) {} + ) { + } public function runInstall( Package $package, diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index db0151cb..ea633c7a 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -58,13 +58,13 @@ public static function phpPathProvider(): array '/usr/bin/php-config8.0', '/usr/bin/php-config7.4', ], - static fn(string $phpConfigPath) => file_exists($phpConfigPath) + static fn (string $phpConfigPath) => file_exists($phpConfigPath) && is_executable($phpConfigPath), ); return array_combine( $possiblePhpConfigPaths, - array_map(static fn(string $phpConfigPath) => [$phpConfigPath], $possiblePhpConfigPaths), + array_map(static fn (string $phpConfigPath) => [$phpConfigPath], $possiblePhpConfigPaths), ); } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index c27fff3c..551095f9 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -49,7 +49,7 @@ public static function validPackageAndVersions(): array ]; return array_combine( - array_map(static fn(array $data) => $data[0], $packages), + array_map(static fn (array $data) => $data[0], $packages), $packages, ); } From c96c1f6759125b8af88ff34bd28e9e74a4df584c Mon Sep 17 00:00:00 2001 From: vinchan Date: Fri, 6 Feb 2026 13:57:43 +0800 Subject: [PATCH 5/6] fix(*):optimizing PHPUnit error issues --- src/Command/CommandHelper.php | 8 +++----- src/Command/InstallCommand.php | 12 ++++++------ test/integration/Command/ShowCommandTest.php | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index c1e04d32..af9cd8f2 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,9 +64,7 @@ final class CommandHelper private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; - private function __construct() - { - } + private function __construct() {} public static function configurePhpConfigOptions(Command $command): void { @@ -283,7 +281,7 @@ public static function requestedNameAndVersionPairs(InputInterface $input): arra $requestedPackages = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); if (! is_array($requestedPackages)) { - // 兼容单个字符串的情况(向后兼容) + // Backwards compatibility: accept a single string instead of array if (! is_string($requestedPackages) || $requestedPackages === '') { throw new InvalidArgumentException('No package was requested for installation'); } @@ -334,7 +332,7 @@ public static function requestedNameAndVersionPair(InputInterface $input): Reque throw new InvalidArgumentException('No package was requested for installation'); } - // 如果传入多个,只返回第一个(向后兼容单包逻辑) + // If multiple packages provided, return only the first one for backwards compatibility return $pairs[0]; } diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index fffa902d..c7958fbb 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -78,7 +78,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); - // 解析所有待安装的包 + // Parse all packages to install try { $requestedPackagesList = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { @@ -105,7 +105,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $totalPackages = count($requestedPackagesList); - // 如果只有一个包,使用原有的详细输出模式 + // If only one package, use the original detailed output mode if ($totalPackages === 1) { return $this->installSinglePackage( $requestedPackagesList[0], @@ -116,7 +116,7 @@ public function execute(InputInterface $input, OutputInterface $output): int ); } - // 多包安装模式 + // Multiple packages installation mode $this->io->write(sprintf( 'Installing %d extensions...', $totalPackages, @@ -183,7 +183,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $this->io->write(''); } - // 输出总结 + // Output summary $this->io->write('====================================='); $this->io->write(sprintf( 'Installation Summary: %d succeeded, %d failed out of %d total', @@ -205,7 +205,7 @@ public function execute(InputInterface $input, OutputInterface $output): int } /** - * 安装单个扩展包 + * Install a single extension package * * @throws UnableToResolveRequirement * @throws BundledPhpExtensionRefusal @@ -256,7 +256,7 @@ private function installSinglePackage( ); } - // 验证配置选项 + // Validate configure options CommandHelper::bindConfigureOptionsFromPackage($this, $package, $input); $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($package, $input); diff --git a/test/integration/Command/ShowCommandTest.php b/test/integration/Command/ShowCommandTest.php index c1a4948f..501a7d0f 100644 --- a/test/integration/Command/ShowCommandTest.php +++ b/test/integration/Command/ShowCommandTest.php @@ -65,7 +65,7 @@ public function testExecuteWithAvailableConstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); @@ -112,7 +112,7 @@ public function testExecuteWithAvailableUnconstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); @@ -159,7 +159,7 @@ public function testExecuteWithOnlyUnconstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); From 38b72e674d7de96fb5ac0bc46a4060ae6aa27f2c Mon Sep 17 00:00:00 2001 From: vinchan Date: Fri, 6 Feb 2026 13:59:00 +0800 Subject: [PATCH 6/6] style(*):code style --- src/Command/CommandHelper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index af9cd8f2..65da7668 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,7 +64,9 @@ final class CommandHelper private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; - private function __construct() {} + private function __construct() + { + } public static function configurePhpConfigOptions(Command $command): void {