diff --git a/src/RequirementChecker/AppRequirementsFactory.php b/src/RequirementChecker/AppRequirementsFactory.php index db83165f2..5aede6d6e 100644 --- a/src/RequirementChecker/AppRequirementsFactory.php +++ b/src/RequirementChecker/AppRequirementsFactory.php @@ -28,11 +28,39 @@ final class AppRequirementsFactory { private const SELF_PACKAGE = null; + public function createUnfiltered( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->all(); + } + public function create( ComposerJson $composerJson, ComposerLock $composerLock, CompressionAlgorithm $compressionAlgorithm, ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->build(); + } + + private function createBuilder( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): RequirementsBuilder { $requirementsBuilder = new RequirementsBuilder(); self::retrievePhpVersionRequirements($requirementsBuilder, $composerJson, $composerLock); @@ -40,7 +68,7 @@ public function create( self::collectComposerLockExtensionRequirements($composerLock, $requirementsBuilder); self::collectComposerJsonExtensionRequirements($composerJson, $requirementsBuilder); - return $requirementsBuilder->build(); + return $requirementsBuilder; } private static function retrievePhpVersionRequirements( diff --git a/src/RequirementChecker/Requirement.php b/src/RequirementChecker/Requirement.php index fe1d8e3ef..c5752b275 100644 --- a/src/RequirementChecker/Requirement.php +++ b/src/RequirementChecker/Requirement.php @@ -86,6 +86,35 @@ public static function forRequiredExtension(string $extension, ?string $packageN ); } + public static function forProvidedExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::PROVIDED_EXTENSION, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application provides the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" provides the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application does not require the extension "%s", it is provided by the application itself.', + $extension, + ) + : sprintf( + 'This application does not require the extension "%s", it is provided by the package "%s".', + $packageName, + $extension, + ), + ); + } + public static function forConflictingExtension(string $extension, ?string $packageName): self { return new self( diff --git a/src/RequirementChecker/RequirementType.php b/src/RequirementChecker/RequirementType.php index e5edb1882..89d5e642a 100644 --- a/src/RequirementChecker/RequirementType.php +++ b/src/RequirementChecker/RequirementType.php @@ -18,5 +18,6 @@ enum RequirementType: string { case PHP = 'php'; case EXTENSION = 'extension'; + case PROVIDED_EXTENSION = 'provided-extension'; case EXTENSION_CONFLICT = 'extension-conflict'; } diff --git a/src/RequirementChecker/RequirementsBuilder.php b/src/RequirementChecker/RequirementsBuilder.php index 0ce061248..d4518edc9 100644 --- a/src/RequirementChecker/RequirementsBuilder.php +++ b/src/RequirementChecker/RequirementsBuilder.php @@ -49,6 +49,40 @@ public function addConflictingExtension(Extension $extension, ?string $source): $this->conflictingExtensions[$extension->name][] = $source; } + public function all(): Requirements + { + $requirements = $this->predefinedRequirements; + + foreach ($this->getUnfilteredSortedRequiredExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forRequiredExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedProvidedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forProvidedExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedConflictedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forConflictingExtension( + $extensionName, + $source, + ); + } + } + + return new Requirements($requirements); + } + public function build(): Requirements { $requirements = $this->predefinedRequirements; @@ -74,6 +108,32 @@ public function build(): Requirements return new Requirements($requirements); } + /** + * @return array> + */ + private function getUnfilteredSortedRequiredExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->requiredExtensions, + ), + ); + } + + /** + * @return array> + */ + private function getSortedProvidedExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->providedExtensions, + ), + ); + } + /** * @return array> */ diff --git a/tests/RequirementChecker/RequirementTest.php b/tests/RequirementChecker/RequirementTest.php index f72403734..ef30569de 100644 --- a/tests/RequirementChecker/RequirementTest.php +++ b/tests/RequirementChecker/RequirementTest.php @@ -95,6 +95,42 @@ public function test_it_can_be_created_for_an_extension_constraint_for_a_package self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); } + public function test_it_can_be_created_for_a_provided_extension_constraint(): void + { + $requirement = Requirement::forProvidedExtension('mbstring', null); + + $expected = [ + 'type' => 'provided-extension', + 'condition' => 'mbstring', + 'source' => null, + 'message' => 'This application provides the extension "mbstring".', + 'helpMessage' => 'This application does not require the extension "mbstring", it is provided by the application itself.', + ]; + + $actual = $requirement->toArray(); + + self::assertSame($expected, $actual); + self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); + } + + public function test_it_can_be_created_for_a_provided_extension_constraint_for_a_package(): void + { + $requirement = Requirement::forProvidedExtension('mbstring', 'box/test'); + + $expected = [ + 'type' => 'provided-extension', + 'condition' => 'mbstring', + 'source' => 'box/test', + 'message' => 'The package "box/test" provides the extension "mbstring".', + 'helpMessage' => 'This application does not require the extension "box/test", it is provided by the package "mbstring".', + ]; + + $actual = $requirement->toArray(); + + self::assertSame($expected, $actual); + self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); + } + public function test_it_can_be_created_for_a_conflicting_extension_constraint(): void { $requirement = Requirement::forConflictingExtension('mbstring', null); diff --git a/tests/RequirementChecker/RequirementsBuilderTest.php b/tests/RequirementChecker/RequirementsBuilderTest.php index 028ffe97a..559c0ae71 100644 --- a/tests/RequirementChecker/RequirementsBuilderTest.php +++ b/tests/RequirementChecker/RequirementsBuilderTest.php @@ -25,13 +25,19 @@ #[CoversClass(RequirementsBuilder::class)] final class RequirementsBuilderTest extends TestCase { - public function test_it_can_build_requirements_from_an_empty_list(): void + private RequirementsBuilder $builder; + + protected function setUp(): void { - $requirements = (new RequirementsBuilder())->build(); + $this->builder = new RequirementsBuilder(); + } + public function test_it_can_build_requirements_from_an_empty_list(): void + { $expected = new Requirements([]); - self::assertEquals($expected, $requirements); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_predefined_requirements(): void @@ -42,39 +48,36 @@ public function test_it_can_build_requirements_from_predefined_requirements(): v Requirement::forConflictingExtension('http', null), ]; - $builder = new RequirementsBuilder(); - foreach ($predefinedRequirements as $predefinedRequirement) { - $builder->addRequirement($predefinedRequirement); + $this->builder->addRequirement($predefinedRequirement); } $expected = new Requirements($predefinedRequirements); - $actual = $builder->build(); - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_required_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); // Duplicate - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); @@ -86,51 +89,51 @@ public function test_it_can_build_requirements_from_required_extensions(): void Requirement::forRequiredExtension('phar', 'package1'), ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_provided_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package1', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package2', ); - $expected = new Requirements([]); - - $actual = $builder->build(); + $expectedBuiltRequirements = new Requirements([]); + $expectedAllRequirements = new Requirements([ + Requirement::forProvidedExtension('http', 'package1'), + Requirement::forProvidedExtension('http', 'package2'), + ]); - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_can_build_requirements_from_conflicting_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package2', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('phar'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('openssl'), 'package3', ); // Duplicate - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('openssl'), 'package3', ); @@ -142,88 +145,96 @@ public function test_it_can_build_requirements_from_conflicting_extensions(): vo Requirement::forConflictingExtension('phar', 'package1'), ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_removes_extension_requirements_if_they_are_provided(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); - $expected = new Requirements([ + $expectedBuiltRequirements = new Requirements([ Requirement::forRequiredExtension('openssl', 'package3'), Requirement::forRequiredExtension('phar', 'package1'), ]); + $expectedAllRequirements = new Requirements([ + Requirement::forRequiredExtension('http', 'package1'), + Requirement::forRequiredExtension('http', 'package2'), + Requirement::forRequiredExtension('openssl', 'package3'), + Requirement::forRequiredExtension('phar', 'package1'), + Requirement::forProvidedExtension('http', 'package3'), + ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_does_not_remove_extension_conflicts_if_they_are_provided(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); - $expected = new Requirements([ + $expectedBuiltRequirements = new Requirements([ Requirement::forRequiredExtension('openssl', 'package3'), Requirement::forRequiredExtension('phar', 'package1'), ]); + $expectedAllRequirements = new Requirements([ + Requirement::forRequiredExtension('http', 'package1'), + Requirement::forRequiredExtension('http', 'package2'), + Requirement::forRequiredExtension('openssl', 'package3'), + Requirement::forRequiredExtension('phar', 'package1'), + Requirement::forProvidedExtension('http', 'package3'), + ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_can_have_an_extension_that_is_required_and_conflicting_at_the_same_time(): void { // This scenario does not really make sense but ensuring this does not happen is Composer's job not Box. - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package2', ); @@ -233,9 +244,11 @@ public function test_it_can_have_an_extension_that_is_required_and_conflicting_a Requirement::forConflictingExtension('http', 'package2'), ]); - $actual = $builder->build(); + $builtRequirements = $this->builder->build(); + $allRequirements = $this->builder->all(); - self::assertEquals($expected, $actual); + self::assertEquals($expected, $builtRequirements); + self::assertEquals($expected, $allRequirements); } // TODO: this could be solved @@ -243,16 +256,15 @@ public function test_it_does_not_remove_predefined_requirements_even_if_they_are { $predefinedRequirement = Requirement::forRequiredExtension('http', null); - $builder = new RequirementsBuilder(); - $builder->addRequirement($predefinedRequirement); - $builder->addProvidedExtension( + $this->builder->addRequirement($predefinedRequirement); + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); $expected = new Requirements([$predefinedRequirement]); - $actual = $builder->build(); + $actual = $this->builder->build(); self::assertEquals($expected, $actual); } @@ -262,25 +274,28 @@ public function test_it_ensures_the_requirements_built_are_consistent( array $predefinedRequirements, array $requiredExtensionSourcePairs, array $conflictingExtensionSourcePairs, - Requirements $expected, + array $providedExtensionSourcePairs, + Requirements $expectedBuiltRequirements, + Requirements $expectedAllRequirements, ): void { - $builder = new RequirementsBuilder(); - foreach ($predefinedRequirements as $predefinedRequirement) { - $builder->addRequirement($predefinedRequirement); + $this->builder->addRequirement($predefinedRequirement); } foreach ($requiredExtensionSourcePairs as [$requiredExtension, $source]) { - $builder->addRequiredExtension($requiredExtension, $source); + $this->builder->addRequiredExtension($requiredExtension, $source); } foreach ($conflictingExtensionSourcePairs as [$conflictingExtension, $source]) { - $builder->addConflictingExtension($conflictingExtension, $source); + $this->builder->addConflictingExtension($conflictingExtension, $source); } - $actual = $builder->build(); + foreach ($providedExtensionSourcePairs as [$conflictingExtension, $source]) { + $this->builder->addProvidedExtension($conflictingExtension, $source); + } - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public static function requirementsProvider(): iterable @@ -297,6 +312,12 @@ public static function requirementsProvider(): iterable ], [], [], + [], + new Requirements([ + $predefinedRequirementZ, + $predefinedRequirementNull, + $predefinedRequirementA, + ]), new Requirements([ $predefinedRequirementZ, $predefinedRequirementNull, @@ -312,6 +333,12 @@ public static function requirementsProvider(): iterable [new Extension('noop'), 'A'], ], [], + [], + new Requirements([ + Requirement::forRequiredExtension('noop', null), + Requirement::forRequiredExtension('noop', 'A'), + Requirement::forRequiredExtension('noop', 'Z'), + ]), new Requirements([ Requirement::forRequiredExtension('noop', null), Requirement::forRequiredExtension('noop', 'A'), @@ -326,6 +353,11 @@ public static function requirementsProvider(): iterable [new Extension('a-ext'), null], ], [], + [], + new Requirements([ + Requirement::forRequiredExtension('a-ext', null), + Requirement::forRequiredExtension('z-ext', null), + ]), new Requirements([ Requirement::forRequiredExtension('a-ext', null), Requirement::forRequiredExtension('z-ext', null), @@ -340,6 +372,12 @@ public static function requirementsProvider(): iterable [new Extension('noop'), null], [new Extension('noop'), 'A'], ], + [], + new Requirements([ + Requirement::forConflictingExtension('noop', null), + Requirement::forConflictingExtension('noop', 'A'), + Requirement::forConflictingExtension('noop', 'Z'), + ]), new Requirements([ Requirement::forConflictingExtension('noop', null), Requirement::forConflictingExtension('noop', 'A'), @@ -354,10 +392,61 @@ public static function requirementsProvider(): iterable [new Extension('z-ext'), null], [new Extension('a-ext'), null], ], + [], + new Requirements([ + Requirement::forConflictingExtension('a-ext', null), + Requirement::forConflictingExtension('z-ext', null), + ]), new Requirements([ Requirement::forConflictingExtension('a-ext', null), Requirement::forConflictingExtension('z-ext', null), ]), ]; + + yield 'provided extension sources' => [ + [], + [], + [], + [ + [new Extension('noop'), 'Z'], + [new Extension('noop'), null], + [new Extension('noop'), 'A'], + ], + new Requirements([]), + new Requirements([ + Requirement::forProvidedExtension('noop', null), + Requirement::forProvidedExtension('noop', 'A'), + Requirement::forProvidedExtension('noop', 'Z'), + ]), + ]; + + yield 'provided extensions' => [ + [], + [], + [], + [ + [new Extension('z-ext'), null], + [new Extension('a-ext'), null], + ], + new Requirements([]), + new Requirements([ + Requirement::forProvidedExtension('a-ext', null), + Requirement::forProvidedExtension('z-ext', null), + ]), + ]; + } + + private function assertBuiltRequirementsEquals(Requirements $expected): void + { + $actual = $this->builder->build(); + + self::assertEquals($expected, $actual); + } + + private function assertAllRequirementsEquals(Requirements $expected): void + { + $actual = $this->builder->all(); + + self::assertEquals($expected, $actual); } }