Skip to content

Commit 3a5a68a

Browse files
committed
Fix extensions with replaces defined from conflicting
1 parent 0eae70a commit 3a5a68a

File tree

5 files changed

+107
-13
lines changed

5 files changed

+107
-13
lines changed

src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44

55
namespace Php\Pie\ComposerIntegration;
66

7+
use Composer\Composer;
78
use Composer\Package\CompletePackage;
89
use Composer\Pcre\Preg;
910
use Composer\Repository\PlatformRepository;
1011
use Composer\Semver\VersionParser;
1112
use Php\Pie\ExtensionName;
13+
use Php\Pie\Platform\InstalledPiePackages;
1214
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
1315
use UnexpectedValueException;
1416

17+
use function in_array;
1518
use function str_replace;
19+
use function str_starts_with;
1620
use function strtolower;
1721

1822
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
1923
class PhpBinaryPathBasedPlatformRepository extends PlatformRepository
2024
{
2125
private VersionParser $versionParser;
2226

23-
public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $extensionBeingInstalled)
27+
public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, InstalledPiePackages $installedPiePackages, ExtensionName|null $extensionBeingInstalled)
2428
{
2529
$this->versionParser = new VersionParser();
2630
$this->packages = [];
@@ -32,6 +36,18 @@ public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $ex
3236

3337
$extVersions = $phpBinaryPath->extensions();
3438

39+
$piePackages = $installedPiePackages->allPiePackages($composer);
40+
$extensionsBeingReplacedByPiePackages = [];
41+
foreach ($piePackages as $piePackage) {
42+
foreach ($piePackage->composerPackage()->getReplaces() as $replaceLink) {
43+
if (! str_starts_with($replaceLink->getTarget(), 'ext-') || ! ExtensionName::isValidExtensionName($replaceLink->getTarget())) {
44+
continue;
45+
}
46+
47+
$extensionsBeingReplacedByPiePackages[] = ExtensionName::normaliseFromString($replaceLink->getTarget())->name();
48+
}
49+
}
50+
3551
foreach ($extVersions as $extension => $extensionVersion) {
3652
/**
3753
* If the extension we're trying to exclude is not excluded from this list if it is already installed
@@ -43,6 +59,15 @@ public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $ex
4359
continue;
4460
}
4561

62+
/**
63+
* If any extensions present have `replaces`, we need to remove them otherwise it conflicts too
64+
*
65+
* @link https://github.com/php/pie/issues/161
66+
*/
67+
if (in_array($extension, $extensionsBeingReplacedByPiePackages)) {
68+
continue;
69+
}
70+
4671
$this->addPackage($this->packageForExtension($extension, $extensionVersion));
4772
}
4873

src/ComposerIntegration/PieComposerInstaller.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Composer\IO\IOInterface;
1010
use Composer\Repository\PlatformRepository;
1111
use Php\Pie\ExtensionName;
12+
use Php\Pie\Platform\InstalledPiePackages;
1213
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
1314
use Webmozart\Assert\Assert;
1415

@@ -21,13 +22,15 @@ class PieComposerInstaller extends Installer
2122
{
2223
private PhpBinaryPath|null $phpBinaryPath = null;
2324
private ExtensionName|null $extensionBeingInstalled = null;
25+
private Composer|null $composer = null;
2426

2527
protected function createPlatformRepo(bool $forUpdate): PlatformRepository
2628
{
2729
Assert::notNull($this->phpBinaryPath, '$phpBinaryPath was not set, maybe createWithPhpBinary was not used?');
2830
Assert::notNull($this->extensionBeingInstalled, '$extensionBeingInstalled was not set, maybe createWithPhpBinary was not used?');
31+
Assert::notNull($this->composer, '$composer was not set, maybe createWithPhpBinary was not used?');
2932

30-
return new PhpBinaryPathBasedPlatformRepository($this->phpBinaryPath, $this->extensionBeingInstalled);
33+
return new PhpBinaryPathBasedPlatformRepository($this->phpBinaryPath, $this->composer, new InstalledPiePackages(), $this->extensionBeingInstalled);
3134
}
3235

3336
public static function createWithPhpBinary(
@@ -51,6 +54,7 @@ public static function createWithPhpBinary(
5154

5255
$composerInstaller->phpBinaryPath = $php;
5356
$composerInstaller->extensionBeingInstalled = $extensionBeingInstalled;
57+
$composerInstaller->composer = $composer;
5458

5559
return $composerInstaller;
5660
}

src/ComposerIntegration/VersionSelectorFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Composer\Repository\RepositorySet;
1111
use Php\Pie\DependencyResolver\DetermineMinimumStability;
1212
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
13+
use Php\Pie\Platform\InstalledPiePackages;
1314
use Php\Pie\Platform\TargetPlatform;
1415

1516
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
@@ -35,7 +36,7 @@ public static function make(
3536
): VersionSelector {
3637
return new VersionSelector(
3738
self::factoryRepositorySet($composer, $requestedPackageAndVersion->version),
38-
new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, null),
39+
new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), null),
3940
);
4041
}
4142
}

src/ExtensionName.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
namespace Php\Pie;
66

77
use Composer\Package\PackageInterface;
8+
use InvalidArgumentException;
89
use Webmozart\Assert\Assert;
910

1011
use function array_key_exists;
11-
use function assert;
1212
use function explode;
1313
use function is_string;
14+
use function preg_match;
15+
use function sprintf;
1416
use function str_starts_with;
1517
use function strlen;
1618
use function substr;
@@ -37,17 +39,22 @@ final class ExtensionName
3739

3840
private function __construct(string $normalisedExtensionName)
3941
{
40-
Assert::regex(
41-
$normalisedExtensionName,
42-
self::VALID_PACKAGE_NAME_REGEX,
43-
'The value %s is not a valid extension name. An extension must start with a letter, and only'
44-
. ' contain alphanumeric characters or underscores',
45-
);
46-
assert($normalisedExtensionName !== '');
42+
if (! self::isValidExtensionName($normalisedExtensionName)) {
43+
throw new InvalidArgumentException(sprintf(
44+
'The value %s is not a valid extension name. An extension must start with a letter, and only contain alphanumeric characters or underscores',
45+
$normalisedExtensionName,
46+
));
47+
}
4748

4849
$this->normalisedExtensionName = $normalisedExtensionName;
4950
}
5051

52+
/** @psalm-assert-if-true non-empty-string $extensionName */
53+
public static function isValidExtensionName(string $extensionName): bool
54+
{
55+
return preg_match(self::VALID_PACKAGE_NAME_REGEX, $extensionName) !== false;
56+
}
57+
5158
public static function determineFromComposerPackage(PackageInterface $package): self
5259
{
5360
$phpExt = $package->getPhpExt();

test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44

55
namespace Php\PieUnitTest\ComposerIntegration;
66

7+
use Composer\Composer;
8+
use Composer\Package\CompletePackage;
9+
use Composer\Package\Link;
710
use Composer\Package\PackageInterface;
11+
use Composer\Semver\Constraint\Constraint;
812
use Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository;
13+
use Php\Pie\DependencyResolver\Package;
914
use Php\Pie\ExtensionName;
15+
use Php\Pie\Platform\InstalledPiePackages;
1016
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
1117
use PHPUnit\Framework\Attributes\CoversClass;
1218
use PHPUnit\Framework\TestCase;
@@ -18,6 +24,11 @@ final class PhpBinaryPathBasedPlatformRepositoryTest extends TestCase
1824
{
1925
public function testPlatformRepositoryContainsExpectedPacakges(): void
2026
{
27+
$composer = $this->createMock(Composer::class);
28+
29+
$installedPiePackages = $this->createMock(InstalledPiePackages::class);
30+
$installedPiePackages->method('allPiePackages')->willReturn([]);
31+
2132
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
2233
$phpBinaryPath->expects(self::once())
2334
->method('version')
@@ -31,7 +42,7 @@ public function testPlatformRepositoryContainsExpectedPacakges(): void
3142
'another' => '1.2.3-alpha.34',
3243
]);
3344

34-
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, null);
45+
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, null);
3546

3647
self::assertSame(
3748
[
@@ -50,6 +61,11 @@ public function testPlatformRepositoryContainsExpectedPacakges(): void
5061

5162
public function testPlatformRepositoryExcludesExtensionBeingInstalled(): void
5263
{
64+
$composer = $this->createMock(Composer::class);
65+
66+
$installedPiePackages = $this->createMock(InstalledPiePackages::class);
67+
$installedPiePackages->method('allPiePackages')->willReturn([]);
68+
5369
$extensionBeingInstalled = ExtensionName::normaliseFromString('extension_being_installed');
5470

5571
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
@@ -63,7 +79,48 @@ public function testPlatformRepositoryExcludesExtensionBeingInstalled(): void
6379
'extension_being_installed' => '1.2.3',
6480
]);
6581

66-
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $extensionBeingInstalled);
82+
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, $extensionBeingInstalled);
83+
84+
self::assertSame(
85+
[
86+
'php:8.1.0',
87+
'ext-foo:8.1.0',
88+
],
89+
array_map(
90+
static fn (PackageInterface $package): string => $package->getName() . ':' . $package->getPrettyVersion(),
91+
$platformRepository->getPackages(),
92+
),
93+
);
94+
}
95+
96+
public function testPlatformRepositoryExcludesReplacedExtensions(): void
97+
{
98+
$composer = $this->createMock(Composer::class);
99+
100+
$composerPackage = $this->createMock(CompletePackage::class);
101+
$composerPackage->method('getPrettyName')->willReturn('myvendor/extension-with-replaces');
102+
$composerPackage->method('getReplaces')->willReturn([
103+
new Link('myvendor/extension-with-replaces', 'ext-replaced_extension', new Constraint('==', '*')),
104+
]);
105+
$installedPiePackages = $this->createMock(InstalledPiePackages::class);
106+
$installedPiePackages->method('allPiePackages')->willReturn([
107+
Package::fromComposerCompletePackage($composerPackage),
108+
]);
109+
110+
$extensionBeingInstalled = ExtensionName::normaliseFromString('extension_being_installed');
111+
112+
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
113+
$phpBinaryPath->expects(self::once())
114+
->method('version')
115+
->willReturn('8.1.0');
116+
$phpBinaryPath->expects(self::once())
117+
->method('extensions')
118+
->willReturn([
119+
'foo' => '8.1.0',
120+
'replaced_extension' => '3.0.0',
121+
]);
122+
123+
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, $extensionBeingInstalled);
67124

68125
self::assertSame(
69126
[

0 commit comments

Comments
 (0)