diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php
index 0860038e..09983648 100644
--- a/src/Command/BuildCommand.php
+++ b/src/Command/BuildCommand.php
@@ -41,9 +41,9 @@ public function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
-
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
$this->container,
@@ -58,7 +58,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion);
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
$output->writeln(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
@@ -80,7 +85,13 @@ public function execute(InputInterface $input, OutputInterface $output): int
);
try {
- ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion);
+ ($this->composerIntegrationHandler)(
+ $package,
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
} catch (ComposerRunFailed $composerRunFailed) {
$output->writeln('' . $composerRunFailed->getMessage() . '');
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 8e25165b..6fc1368f 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -39,6 +39,7 @@ final class CommandHelper
private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path';
private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs';
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
+ private const OPTION_FORCE = 'force';
/** @psalm-suppress UnusedConstructor */
private function __construct()
@@ -86,6 +87,12 @@ public static function configureDownloadBuildInstallOptions(Command $command): v
InputOption::VALUE_NONE,
'Specify this to skip attempting to enable the extension in php.ini',
);
+ $command->addOption(
+ self::OPTION_FORCE,
+ null,
+ InputOption::VALUE_NONE,
+ 'To attempt to install a version that doesn\'t match the version constraints from the meta-data, for instance to install an older version than recommended, or when the signature is not available.',
+ );
self::configurePhpConfigOptions($command);
@@ -166,6 +173,11 @@ public static function determineAttemptToSetupIniFile(InputInterface $input): bo
return ! $input->hasOption(self::OPTION_SKIP_ENABLE_EXTENSION) || ! $input->getOption(self::OPTION_SKIP_ENABLE_EXTENSION);
}
+ public static function determineForceInstallingPackageVersion(InputInterface $input): bool
+ {
+ return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE);
+ }
+
public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null
{
if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) {
diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php
index f6616290..4a7439b1 100644
--- a/src/Command/DownloadCommand.php
+++ b/src/Command/DownloadCommand.php
@@ -43,9 +43,9 @@ public function execute(InputInterface $input, OutputInterface $output): int
{
CommandHelper::validateInput($input, $this);
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
-
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
$this->container,
@@ -60,11 +60,22 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion);
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix()));
try {
- ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion);
+ ($this->composerIntegrationHandler)(
+ $package,
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
} catch (ComposerRunFailed $composerRunFailed) {
$output->writeln('' . $composerRunFailed->getMessage() . '');
diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php
index 9f1f806f..0d03e934 100644
--- a/src/Command/InfoCommand.php
+++ b/src/Command/InfoCommand.php
@@ -58,7 +58,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion);
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ CommandHelper::determineForceInstallingPackageVersion($input),
+ );
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix()));
$output->writeln(sprintf('Extension name: %s', $package->extensionName->name()));
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index aca34af2..0640b5e4 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -46,9 +46,9 @@ public function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('This command may need elevated privileges, and may prompt you for your password.');
}
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
-
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
$this->container,
@@ -63,7 +63,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion);
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
$output->writeln(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
@@ -85,7 +90,13 @@ public function execute(InputInterface $input, OutputInterface $output): int
);
try {
- ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion);
+ ($this->composerIntegrationHandler)(
+ $package,
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
} catch (ComposerRunFailed $composerRunFailed) {
$output->writeln('' . $composerRunFailed->getMessage() . '');
diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php
index d5e33ca4..05d9c8b1 100644
--- a/src/ComposerIntegration/ComposerIntegrationHandler.php
+++ b/src/ComposerIntegration/ComposerIntegrationHandler.php
@@ -5,6 +5,7 @@
namespace Php\Pie\ComposerIntegration;
use Composer\Composer;
+use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Installer;
use Composer\Json\JsonManipulator;
use Php\Pie\DependencyResolver\Package;
@@ -27,8 +28,13 @@ public function __construct(
) {
}
- public function __invoke(Package $package, Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): void
- {
+ public function __invoke(
+ Package $package,
+ Composer $composer,
+ TargetPlatform $targetPlatform,
+ RequestedPackageAndVersion $requestedPackageAndVersion,
+ bool $forceInstallPackageVersion,
+ ): void {
$versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform);
$recommendedRequireVersion = $requestedPackageAndVersion->version;
@@ -64,6 +70,7 @@ public function __invoke(Package $package, Composer $composer, TargetPlatform $t
->setInstall(true)
->setIgnoredTypes([])
->setDryRun(false)
+ ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion))
->setDownloadOnly(false);
if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) {
diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php
index 98309fe0..8179089f 100644
--- a/src/DependencyResolver/DependencyResolver.php
+++ b/src/DependencyResolver/DependencyResolver.php
@@ -11,5 +11,10 @@
interface DependencyResolver
{
/** @throws UnableToResolveRequirement */
- public function __invoke(Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): Package;
+ public function __invoke(
+ Composer $composer,
+ TargetPlatform $targetPlatform,
+ RequestedPackageAndVersion $requestedPackageAndVersion,
+ bool $forceInstallPackageVersion,
+ ): Package;
}
diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php
index 8bbdc020..d2bdbd28 100644
--- a/src/DependencyResolver/ResolveDependencyWithComposer.php
+++ b/src/DependencyResolver/ResolveDependencyWithComposer.php
@@ -5,6 +5,7 @@
namespace Php\Pie\DependencyResolver;
use Composer\Composer;
+use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
use Composer\Package\CompletePackageInterface;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\ComposerIntegration\VersionSelectorFactory;
@@ -23,13 +24,18 @@ public function __construct(
) {
}
- public function __invoke(Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): Package
- {
+ public function __invoke(
+ Composer $composer,
+ TargetPlatform $targetPlatform,
+ RequestedPackageAndVersion $requestedPackageAndVersion,
+ bool $forceInstallPackageVersion,
+ ): Package {
$versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform);
$package = $versionSelector->findBestCandidate(
$requestedPackageAndVersion->package,
$requestedPackageAndVersion->version,
+ platformRequirementFilter: PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion),
io: $this->arrayCollectionIo,
);
diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php
index 2baf4103..c15bc88e 100644
--- a/test/integration/Command/DownloadCommandTest.php
+++ b/test/integration/Command/DownloadCommandTest.php
@@ -151,4 +151,26 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void
// 1.0.0 is only compatible with PHP 8.3.0
$this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE . ':1.0.0']);
}
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ #[RequiresPhp('<8.2')]
+ public function testDownloadCommandPassesWhenUsingIncompatiblePhpVersionWithForceOption(): void
+ {
+ // 1.0.1 is only compatible with PHP 8.3.0
+ $incompatiblePackage = self::TEST_PACKAGE . ':1.0.1';
+
+ $this->commandTester->execute(
+ [
+ 'requested-package-and-version' => $incompatiblePackage,
+ '--force' => true,
+ ],
+ );
+
+ $this->commandTester->assertCommandIsSuccessful();
+
+ $outputString = $this->commandTester->getDisplay();
+
+ self::assertStringContainsString('Found package: ' . $incompatiblePackage . ' which provides', $outputString);
+ self::assertStringContainsString('Extracted ' . $incompatiblePackage . ' source to', $outputString);
+ }
}
diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
index 1477b39c..43d5799e 100644
--- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
+++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php
@@ -95,6 +95,7 @@ public function testDependenciesAreResolvedToExpectedVersions(
),
$targetPlatform,
$requestedPackageAndVersion,
+ false,
);
self::assertSame($expectedVersion, $package->version);
diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
index 1fff415a..6cdd9a4b 100644
--- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
+++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
@@ -64,7 +64,7 @@ public function testPackageThatCanBeResolved(): void
$package = (new ResolveDependencyWithComposer(
$this->createMock(QuieterConsoleIO::class),
- ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'));
+ ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'), false);
self::assertSame('asgrim/example-pie-extension', $package->name);
self::assertStringStartsWith('1.', $package->version);
@@ -118,9 +118,51 @@ public function testPackageThatCannotBeResolvedThrowsException(array $platformOv
$package,
$version,
),
+ false,
);
}
+ /**
+ * @param array $platformOverrides
+ * @param non-empty-string $package
+ * @param non-empty-string $version
+ */
+ #[DataProvider('unresolvableDependencies')]
+ public function testUnresolvedPackageCanBeInstalledWithForceOption(array $platformOverrides, string $package, string $version): void
+ {
+ $phpBinaryPath = $this->createMock(PhpBinaryPath::class);
+ $phpBinaryPath->expects(self::once())
+ ->method('version')
+ ->willReturn($platformOverrides['php']);
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $phpBinaryPath,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $this->expectException(UnableToResolveRequirement::class);
+
+ $package = (new ResolveDependencyWithComposer(
+ $this->createMock(QuieterConsoleIO::class),
+ ))(
+ $this->composer,
+ $targetPlatform,
+ new RequestedPackageAndVersion(
+ $package,
+ $version,
+ ),
+ true,
+ );
+
+ self::assertSame('asgrim/example-pie-extension', $package->name);
+ self::assertStringStartsWith('1.', $package->version);
+ }
+
public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void
{
$pkg = new CompletePackage('test-vendor/test-package', '1.0.0.0', '1.0.0');
@@ -164,6 +206,7 @@ public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void
'test-vendor/test-package',
'1.0.0',
),
+ false,
);
}
@@ -210,6 +253,7 @@ public function testNtsOnlyPackageCannotBeInstalledOnZtsSystem(): void
'test-vendor/test-package',
'1.0.0',
),
+ false,
);
}
@@ -256,6 +300,7 @@ public function testExtensionCanOnlyBeInstalledIfOsFamilyIsCompatible(): void
'test-vendor/test-package',
'1.0.0',
),
+ false,
);
}
@@ -302,6 +347,7 @@ public function testExtensionCanOnlyBeInstalledIfOsFamilyIsNotInCompatible(): vo
'test-vendor/test-package',
'1.0.0',
),
+ false,
);
}
}