Skip to content

Commit cc3b93b

Browse files
authored
Merge pull request #203 from asgrim/fix-replace-conflict
Fix replace conflict bug
2 parents 0eae70a + 32d0515 commit cc3b93b

File tree

7 files changed

+178
-14
lines changed

7 files changed

+178
-14
lines changed

src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@
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;
20+
use function strlen;
1621
use function strtolower;
22+
use function substr;
1723

1824
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
1925
class PhpBinaryPathBasedPlatformRepository extends PlatformRepository
2026
{
2127
private VersionParser $versionParser;
2228

23-
public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $extensionBeingInstalled)
29+
public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, InstalledPiePackages $installedPiePackages, ExtensionName|null $extensionBeingInstalled)
2430
{
2531
$this->versionParser = new VersionParser();
2632
$this->packages = [];
@@ -32,6 +38,22 @@ public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $ex
3238

3339
$extVersions = $phpBinaryPath->extensions();
3440

41+
$piePackages = $installedPiePackages->allPiePackages($composer);
42+
$extensionsBeingReplacedByPiePackages = [];
43+
foreach ($piePackages as $piePackage) {
44+
foreach ($piePackage->composerPackage()->getReplaces() as $replaceLink) {
45+
$target = $replaceLink->getTarget();
46+
if (
47+
! str_starts_with($target, 'ext-')
48+
|| ! ExtensionName::isValidExtensionName(substr($target, strlen('ext-')))
49+
) {
50+
continue;
51+
}
52+
53+
$extensionsBeingReplacedByPiePackages[] = ExtensionName::normaliseFromString($replaceLink->getTarget())->name();
54+
}
55+
}
56+
3557
foreach ($extVersions as $extension => $extensionVersion) {
3658
/**
3759
* If the extension we're trying to exclude is not excluded from this list if it is already installed
@@ -43,6 +65,15 @@ public function __construct(PhpBinaryPath $phpBinaryPath, ExtensionName|null $ex
4365
continue;
4466
}
4567

68+
/**
69+
* If any extensions present have `replaces`, we need to remove them otherwise it conflicts too
70+
*
71+
* @link https://github.com/php/pie/issues/161
72+
*/
73+
if (in_array($extension, $extensionsBeingReplacedByPiePackages)) {
74+
continue;
75+
}
76+
4677
$this->addPackage($this->packageForExtension($extension, $extensionVersion));
4778
}
4879

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) >= 1;
56+
}
57+
5158
public static function determineFromComposerPackage(PackageInterface $package): self
5259
{
5360
$phpExt = $package->getPhpExt();

test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php

Lines changed: 58 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,47 @@ 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 = new CompletePackage('myvendor/replaced_extension', '1.2.3.0', '1.2.3');
101+
$composerPackage->setReplaces([
102+
'ext-replaced_extension' => new Link('myvendor/replaced_extension', 'ext-replaced_extension', new Constraint('==', '*')),
103+
]);
104+
$installedPiePackages = $this->createMock(InstalledPiePackages::class);
105+
$installedPiePackages->method('allPiePackages')->willReturn([
106+
Package::fromComposerCompletePackage($composerPackage),
107+
]);
108+
109+
$extensionBeingInstalled = ExtensionName::normaliseFromString('extension_being_installed');
110+
111+
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
112+
$phpBinaryPath->expects(self::once())
113+
->method('version')
114+
->willReturn('8.1.0');
115+
$phpBinaryPath->expects(self::once())
116+
->method('extensions')
117+
->willReturn([
118+
'foo' => '8.1.0',
119+
'replaced_extension' => '3.0.0',
120+
]);
121+
122+
$platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, $extensionBeingInstalled);
67123

68124
self::assertSame(
69125
[

test/unit/ComposerIntegration/VersionSelectorFactoryTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66

77
use Composer\Composer;
88
use Composer\Package\CompletePackage;
9+
use Composer\Package\Link;
910
use Composer\Repository\ArrayRepository;
11+
use Composer\Repository\InstalledRepositoryInterface;
1012
use Composer\Repository\RepositoryManager;
13+
use Composer\Semver\Constraint\Constraint;
1114
use Php\Pie\ComposerIntegration\VersionSelectorFactory;
1215
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
1316
use Php\Pie\Platform\Architecture;
@@ -30,15 +33,25 @@ public function testVersionSelectorFactory(): void
3033
new CompletePackage('foo/bar', '2.0.0.0', '2.0.0'),
3134
]);
3235

36+
$packageWithReplaces = new CompletePackage('already/installed2', '1.2.3.0', '1.2.3');
37+
$packageWithReplaces->setReplaces([
38+
'ext-installed2' => new Link('root/package', 'ext-installed2', new Constraint('==', '*')),
39+
]);
40+
$localRepository = $this->createMock(InstalledRepositoryInterface::class);
41+
$localRepository->method('getPackages')->willReturn([
42+
new CompletePackage('already/installed1', '1.2.3.0', '1.2.3'),
43+
$packageWithReplaces,
44+
]);
45+
3346
$repoMananger = $this->createMock(RepositoryManager::class);
3447
$repoMananger
3548
->expects(self::once())
3649
->method('getRepositories')
3750
->willReturn([$repository]);
51+
$repoMananger->method('getLocalRepository')->willReturn($localRepository);
3852

3953
$composer = $this->createMock(Composer::class);
4054
$composer
41-
->expects(self::once())
4255
->method('getRepositoryManager')
4356
->willReturn($repoMananger);
4457

0 commit comments

Comments
 (0)