diff --git a/.github/docs/Dockerfile b/.github/docs/Dockerfile index 9ca685a1..00d58d57 100644 --- a/.github/docs/Dockerfile +++ b/.github/docs/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/roave/docbooktool:1.16.0 AS builder +FROM ghcr.io/roave/docbooktool:1.19.0 AS builder COPY ./.github/docs/templates /docs-src/templates COPY ./docs /docs-src/book diff --git a/.github/docs/templates/online.twig b/.github/docs/templates/online.twig index efc704af..16845a96 100644 --- a/.github/docs/templates/online.twig +++ b/.github/docs/templates/online.twig @@ -5,6 +5,8 @@ PIE Documentation + + @@ -47,6 +84,10 @@ * @param {string} title */ function loadDocBookNavigation(title) { + document.querySelectorAll('table').forEach((table) => { + table.classList.add("table"); + }) + /** * @param {NodeListOf} unselectedListElements * @param {HTMLElement} selectedListElement @@ -184,5 +225,7 @@ loadDocBookNavigation("PIE Documentation"); + + diff --git a/bin/pie b/bin/pie index 88443670..2b907bb1 100755 --- a/bin/pie +++ b/bin/pie @@ -9,6 +9,9 @@ use Php\Pie\Command\BuildCommand; use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; +use Php\Pie\Command\RepositoryAddCommand; +use Php\Pie\Command\RepositoryListCommand; +use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; use Php\Pie\Util\PieVersion; use Symfony\Component\Console\Application; @@ -31,6 +34,9 @@ $application->setCommandLoader(new ContainerCommandLoader( 'install' => InstallCommand::class, 'info' => InfoCommand::class, 'show' => ShowCommand::class, + 'repository:list' => RepositoryListCommand::class, + 'repository:add' => RepositoryAddCommand::class, + 'repository:remove' => RepositoryRemoveCommand::class, ] )); diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 7465b127..610728bd 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -137,7 +137,7 @@ specify a default value in the `configure-options` definition. The `build-path` setting may be used if your source code is not in the root of your repository. For example, if your repository structure is like: -``` +```text / docs/ src/ diff --git a/docs/usage.md b/docs/usage.md index ac4e7179..f54e34ba 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,7 +37,7 @@ COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie Instead of `bin` tag (which represents latest binary-only image) you can also use explicit version (in `x.y.z-bin` format). Use [GitHub registry](https://ghcr.io/php/pie) to find available tags. -> [!IMPORTANT] +> [!IMPORTANT] > Binary-only images don't include PHP runtime so you can't use them for _running_ PIE. This is just an alternative way of distributing PHAR file, you still need to satisfy PIE's runtime requirements on your own. #### Example of PIE working in a Dockerfile @@ -197,7 +197,7 @@ functionality, or to provide paths to libraries not automatically detected. In order to determine what configure options are available for an extension, you may use `pie info /` which will return a list, such as: -``` +```text Configure options: --enable-some-functionality (whether to enable some additional functionality provided) --with-some-library-name=? (Path for some-library) @@ -215,6 +215,45 @@ pie install example/some-extension --with-some-library-name=/path/to/the/lib --e ### Configuring the INI file -At the moment, PIE does not configure the INI file, although this improvement -is planned soon. In the meantime, you must enable the extension after installing -by adding a line such as `extension=foo` to your `php.ini`. +PIE will automatically try to enable the extension by adding `extension=...` or +`zend_extension=...` in the appropriate INI file. If you want to disable this +behaviour, pass the `--skip-enable-extension` flag to your `pie install` +command. The following techniques are used to attempt to enable the extension: + + * `phpenmod`, if using the deb.sury.org distribution + * `docker-php-ext-enable` if using Docker's PHP image + * Add a new file to the "additional .ini file" path, if configured + * Append to the standard php.ini, if configured + +If none of these techniques work, or you used the `--skip-enable-extension` +flag, PIE will warn you that the extension was not enabled, and will note that +you must enable the extension yourself. + +### Adding non-Packagist.org repositories + +Sometimes you may want to install an extension from a package repository other +than Packagist.org (such as [Private Packagist](https://packagist.com/)), or +from a local directory. Since PIE is based heavily on Composer, it is possible +to use some other repository types: + +* `pie repository:add [--with-php-config=...] path /path/to/your/local/extension` +* `pie repository:add [--with-php-config=...] vcs https://github.com/youruser/yourextension` +* `pie repository:add [--with-php-config=...] composer https://repo.packagist.com/your-private-packagist/` +* `pie repository:add [--with-php-config=...] composer packagist.org` + +The `repository:*` commands all support the optional `--with-php-config` flag +to allow you to specify which PHP installation to use (for example, if you have +multiple PHP installations on one machine). The above added repositories can be +removed too, using the inverse `repository:remove` commands: + +* `pie repository:remove [--with-php-config=...] /path/to/your/local/extension` +* `pie repository:remove [--with-php-config=...] https://github.com/youruser/yourextension` +* `pie repository:remove [--with-php-config=...] https://repo.packagist.com/your-private-packagist/` +* `pie repository:remove [--with-php-config=...] packagist.org` + +Note you do not need to specify the repository type in `repository:remove`, +just the URL. + +You can list the repositories for the target PHP installation with: + +* `pie repository:list [--with-php-config=...]` diff --git a/features/install-extensions.feature b/features/install-extensions.feature index 27841d29..20422ed7 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -1,4 +1,4 @@ -Feature: Extensions can be installed with Behat +Feature: Extensions can be installed with PIE Example: The latest version of an extension can be downloaded When I run a command to download the latest version of an extension diff --git a/features/manage-repositories.feature b/features/manage-repositories.feature new file mode 100644 index 00000000..6fd69b50 --- /dev/null +++ b/features/manage-repositories.feature @@ -0,0 +1,11 @@ +Feature: Package repositories can be managed with PIE + + Example: A package repository can be added + Given no repositories have previously been added + When I add a package repository + Then I should see the package repository can be used by PIE + + Example: A package repository can be removed + Given I have previously added a package repository + When I remove the package repository + Then I should see the package repository is not used by PIE diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 6fc1368f..1fc4c7a9 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -4,7 +4,11 @@ namespace Php\Pie\Command; +use Composer\Composer; use Composer\Package\Version\VersionParser; +use Composer\Repository\ComposerRepository; +use Composer\Repository\PathRepository; +use Composer\Repository\VcsRepository; use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\DependencyResolver\Package; @@ -263,4 +267,44 @@ public static function processConfigureOptionsFromInput(Package $package, InputI return $configureOptionsValues; } + + public static function listRepositories(Composer $composer, OutputInterface $output): void + { + $output->writeln('The following repositories are in use for this Target PHP:'); + + foreach ($composer->getRepositoryManager()->getRepositories() as $repo) { + if ($repo instanceof ComposerRepository) { + $repoConfig = $repo->getRepoConfig(); + + $repoUrl = array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : null; + + if ($repoUrl === 'https://repo.packagist.org') { + $output->writeln(' - Packagist'); + continue; + } + + $output->writeln(sprintf(' - Composer (%s)', $repoUrl ?? 'no url?')); + continue; + } + + if ($repo instanceof VcsRepository) { + /** @psalm-suppress InternalMethod */ + $output->writeln(sprintf( + ' - VCS Repository (%s)', + $repo->getDriver()?->getUrl() ?? 'no url?', + )); + continue; + } + + if (! $repo instanceof PathRepository) { + continue; + } + + $repoConfig = $repo->getRepoConfig(); + $output->writeln(sprintf( + ' - Path Repository (%s)', + array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : 'no path?', + )); + } + } } diff --git a/src/Command/RepositoryAddCommand.php b/src/Command/RepositoryAddCommand.php new file mode 100644 index 00000000..57e60be6 --- /dev/null +++ b/src/Command/RepositoryAddCommand.php @@ -0,0 +1,97 @@ +addArgument( + self::ARG_TYPE, + InputArgument::REQUIRED, + 'Specify the type of the repository, e.g. vcs, path, composer', + ); + $this->addArgument( + self::ARG_URL, + InputArgument::REQUIRED, + 'Specify the URL of the repository, e.g. a Github/Gitlab URL, a filesystem path, or Private Packagist URL', + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); + + $type = (string) $input->getArgument(self::ARG_TYPE); + /** @psalm-var 'vcs'|'path'|'composer' $type */ + Assert::inArray($type, self::ALLOWED_TYPES); + + $url = $originalUrl = (string) $input->getArgument(self::ARG_URL); + + if ($type === 'path') { + $url = realpath($originalUrl); + } + + if ($type === 'composer' && str_contains($url, 'packagist.org')) { + // "adding packagist" is really just removing an exclusion + $pieJsonEditor + ->ensureExists() + ->removeRepository('packagist.org'); + } else { + Assert::stringNotEmpty($url, 'Could not resolve ' . $originalUrl . ' to a real path'); + + $pieJsonEditor + ->ensureExists() + ->addRepository($type, $url); + } + + CommandHelper::listRepositories( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $output, + $targetPlatform, + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/Command/RepositoryListCommand.php b/src/Command/RepositoryListCommand.php new file mode 100644 index 00000000..a2d6dddd --- /dev/null +++ b/src/Command/RepositoryListCommand.php @@ -0,0 +1,49 @@ +container, + PieComposerRequest::noOperation( + $output, + CommandHelper::determineTargetPlatformFromInputs($input, $output), + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/Command/RepositoryRemoveCommand.php b/src/Command/RepositoryRemoveCommand.php new file mode 100644 index 00000000..d12f62fb --- /dev/null +++ b/src/Command/RepositoryRemoveCommand.php @@ -0,0 +1,79 @@ +addArgument( + self::ARG_URL, + InputArgument::REQUIRED, + 'Specify the URL of the repository, e.g. a Github/Gitlab URL, or a filesystem path', + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); + + $url = (string) $input->getArgument(self::ARG_URL); + Assert::stringNotEmpty($url); + + if (str_contains($url, 'packagist.org')) { + // "removing packagist" is really just adding an exclusion + $pieJsonEditor + ->ensureExists() + ->excludePackagistOrg(); + } else { + $pieJsonEditor + ->ensureExists() + ->removeRepository($url); + } + + CommandHelper::listRepositories( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $output, + $targetPlatform, + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index d246a496..48d93aa8 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -7,7 +7,6 @@ use Composer\Composer; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; -use Composer\Json\JsonManipulator; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform; @@ -15,8 +14,6 @@ use Psr\Container\ContainerInterface; use function file_exists; -use function file_get_contents; -use function file_put_contents; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class ComposerIntegrationHandler @@ -47,10 +44,11 @@ public function __invoke( // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); - $originalPieJsonContent = file_get_contents($pieComposerJson); - $manipulator = new JsonManipulator($originalPieJsonContent); - $manipulator->addLink('require', $requestedPackageAndVersion->package, $recommendedRequireVersion, true); - file_put_contents($pieComposerJson, $manipulator->getContents()); + $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); + $originalPieJsonContent = $pieJsonEditor->addRequire( + $requestedPackageAndVersion->package, + $recommendedRequireVersion !== '' ? $recommendedRequireVersion : '*', + ); // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); @@ -83,7 +81,7 @@ public function __invoke( if ($resultCode !== Installer::ERROR_NONE) { // Revert composer.json change - file_put_contents($pieComposerJson, $originalPieJsonContent); + $pieJsonEditor->revert($originalPieJsonContent); throw ComposerRunFailed::fromExitCode($resultCode); } diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index d135e611..279348df 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -16,10 +16,6 @@ use Psr\Container\ContainerInterface; use Webmozart\Assert\Assert; -use function file_exists; -use function file_put_contents; -use function mkdir; - /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class PieComposerFactory extends Factory { @@ -52,20 +48,9 @@ public static function createPieComposer( ContainerInterface $container, PieComposerRequest $composerRequest, ): Composer { - $workDir = Platform::getPieWorkingDirectory($composerRequest->targetPlatform); - - if (! file_exists($workDir)) { - mkdir($workDir, recursive: true); - } - $pieComposer = Platform::getPieJsonFilename($composerRequest->targetPlatform); - if (! file_exists($pieComposer)) { - file_put_contents( - $pieComposer, - "{\n}\n", - ); - } + PieJsonEditor::fromTargetPlatform($composerRequest->targetPlatform)->ensureExists(); $io = $container->get(QuieterConsoleIO::class); $composer = (new PieComposerFactory($container, $composerRequest))->createComposer( diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php new file mode 100644 index 00000000..973bfbff --- /dev/null +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -0,0 +1,155 @@ +pieJsonFilename)) { + return $this; + } + + if (! file_exists($this->pieWorkingDirectory)) { + mkdir($this->pieWorkingDirectory, recursive: true); + } + + if (file_put_contents($this->pieJsonFilename, "{\n}\n") === false) { + throw new RuntimeException(sprintf( + 'Failed to create pie.json in %s (working directory: %s)', + $this->pieJsonFilename, + $this->pieWorkingDirectory, + )); + } + + return $this; + } + + /** + * Add a package to the `require` section of the given `pie.json`. Returns + * the original `pie.json` content, in case it needs to be restored later. + * + * @param non-empty-string $package + * @param non-empty-string $version + */ + public function addRequire(string $package, string $version): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + ))->addLink('require', $package, $version); + + return $originalPieJsonContent; + } + + public function revert(string $originalPieJsonContent): void + { + file_put_contents($this->pieJsonFilename, $originalPieJsonContent); + } + + public function excludePackagistOrg(): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->addRepository(self::PACKAGIST_ORG_KEY, false); + + return $originalPieJsonContent; + } + + /** + * Add a repository to the given `pie.json`. Returns the original + * `pie.json` content, in case it needs to be restored later. + * + * @param 'vcs'|'path'|'composer' $type + * @param non-empty-string $url + */ + public function addRepository( + string $type, + string $url, + ): string { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->addRepository($this->normaliseRepositoryName($url), [ + 'type' => $type, + 'url' => $url, + ]); + + return $originalPieJsonContent; + } + + /** + * Remove a repository from the given `pie.json`. Returns the original + * `pie.json` content, in case it needs to be restored later. + * + * @param non-empty-string $name + */ + public function removeRepository( + string $name, + ): string { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->removeRepository($this->normaliseRepositoryName($name)); + + return $originalPieJsonContent; + } + + private function normaliseRepositoryName(string $url): string + { + return rtrim(str_replace('\\', '/', $url), '/'); + } +} diff --git a/src/Container.php b/src/Container.php index 28e1f885..6f8ef418 100644 --- a/src/Container.php +++ b/src/Container.php @@ -13,6 +13,9 @@ use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; +use Php\Pie\Command\RepositoryAddCommand; +use Php\Pie\Command\RepositoryListCommand; +use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; use Php\Pie\ComposerIntegration\MinimalHelperSet; use Php\Pie\ComposerIntegration\QuieterConsoleIO; @@ -46,6 +49,9 @@ public static function factory(): ContainerInterface $container->singleton(InstallCommand::class); $container->singleton(InfoCommand::class); $container->singleton(ShowCommand::class); + $container->singleton(RepositoryListCommand::class); + $container->singleton(RepositoryAddCommand::class); + $container->singleton(RepositoryRemoveCommand::class); $container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO { return new QuieterConsoleIO( diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 801dbe9b..31b2f9a5 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -146,4 +146,43 @@ public function iHaveAnInvalidExtensionInstalled(): void { $this->phpArguments = ['-d', 'extension=invalid_extension']; } + + #[When('I add a package repository')] + public function iAddAPackageRepository(): void + { + $this->runPieCommand(['repository:add', 'path', __DIR__]); + } + + #[Then('I should see the package repository can be used by PIE')] + public function iShouldSeeThePackageRepositoryCanBeUsedByPie(): void + { + Assert::notNull($this->output); + Assert::contains($this->output, 'Path Repository (' . __DIR__ . ')'); + } + + #[Given('I have previously added a package repository')] + public function iHavePreviouslyAddedAPackageRepository(): void + { + $this->noRepositoriesHavePreviouslyBeenAdded(); + $this->iAddAPackageRepository(); + } + + #[Given('no repositories have previously been added')] + public function noRepositoriesHavePreviouslyBeenAdded(): void + { + $this->iRemoveThePackageRepository(); + } + + #[When('I remove the package repository')] + public function iRemoveThePackageRepository(): void + { + $this->runPieCommand(['repository:remove', __DIR__]); + } + + #[Then('I should see the package repository is not used by PIE')] + public function iShouldSeeThePackageRepositoryIsNotUsedByPie(): void + { + Assert::notNull($this->output); + Assert::notContains($this->output, 'Path repository (' . __DIR__ . ')'); + } } diff --git a/test/integration/Command/RepositoryManagementCommandsTest.php b/test/integration/Command/RepositoryManagementCommandsTest.php new file mode 100644 index 00000000..77ab6c56 --- /dev/null +++ b/test/integration/Command/RepositoryManagementCommandsTest.php @@ -0,0 +1,146 @@ +listCommand = new CommandTester(Container::factory()->get(RepositoryListCommand::class)); + $this->addCommand = new CommandTester(Container::factory()->get(RepositoryAddCommand::class)); + $this->removeCommand = new CommandTester(Container::factory()->get(RepositoryRemoveCommand::class)); + + $this->addCommand->execute([ + 'type' => 'composer', + 'url' => self::PACKAGIST_ORG_URL, + ]); + $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL . '.git']); + } + + public function testPathRepositoriesCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist']); + + $this->addCommand->execute([ + 'type' => 'path', + 'url' => self::EXAMPLE_PATH_REPOSITORY_URL, + ]); + + $this->assertRepositoryListDisplayed( + [ + 'Path Repository (' . self::EXAMPLE_PATH_REPOSITORY_URL . ')', + 'Packagist', + ], + ); + + $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); + $this->assertRepositoryListDisplayed(['Packagist']); + } + + public function testPathRepositoriesWithTrailingSlashesCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist']); + + $this->addCommand->execute([ + 'type' => 'path', + 'url' => self::EXAMPLE_PATH_REPOSITORY_URL . '/', + ]); + + $this->assertRepositoryListDisplayed( + [ + 'Path Repository (' . self::EXAMPLE_PATH_REPOSITORY_URL . ')', + 'Packagist', + ], + ); + + $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL . '/']); + $this->assertRepositoryListDisplayed(['Packagist']); + } + + public function testVcsRepositoriesCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist']); + + $this->addCommand->execute([ + 'type' => 'vcs', + 'url' => self::EXAMPLE_VCS_REPOSITORY_URL, + ]); + + $this->assertRepositoryListDisplayed( + [ + 'VCS Repository (' . self::EXAMPLE_VCS_REPOSITORY_URL . '.git)', + 'Packagist', + ], + ); + + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); + $this->assertRepositoryListDisplayed(['Packagist']); + } + + public function testPackagistOrgCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist']); + + $this->removeCommand->execute(['url' => self::PACKAGIST_ORG_URL]); + + $this->assertRepositoryListDisplayed([]); + + $this->addCommand->execute([ + 'type' => 'composer', + 'url' => self::PACKAGIST_ORG_URL, + ]); + $this->assertRepositoryListDisplayed(['Packagist']); + } + + /** @param list $expectedRepositories */ + private function assertRepositoryListDisplayed(array $expectedRepositories): void + { + $this->listCommand->execute([]); + $this->listCommand->assertCommandIsSuccessful(); + + $outputString = $this->listCommand->getDisplay(); + + self::assertEquals( + $expectedRepositories, + array_values(array_map( + static fn ($line) => substr($line, 4), + array_filter( + explode(PHP_EOL, $outputString), + static fn ($line): bool => str_starts_with($line, ' - '), + ), + )), + ); + } +} diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 34183746..f70eb932 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -4,7 +4,13 @@ namespace Php\PieUnitTest\Command; +use Composer\Composer; use Composer\Package\CompletePackage; +use Composer\Repository\ComposerRepository; +use Composer\Repository\PathRepository; +use Composer\Repository\RepositoryManager; +use Composer\Repository\Vcs\GitHubDriver; +use Composer\Repository\VcsRepository; use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\Command\CommandHelper; @@ -22,10 +28,13 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\NullOutput; use function array_combine; use function array_map; +use function str_replace; +use function trim; #[CoversClass(CommandHelper::class)] final class CommandHelperTest extends TestCase @@ -175,4 +184,48 @@ public function testWindowsMachinesCannotUseWithPhpizePathOption(): void $this->expectExceptionMessage('The --with-phpize-path=/path/to/phpize cannot be used on Windows.'); CommandHelper::determineTargetPlatformFromInputs($input, $output); } + + public function testListRepositories(): void + { + $output = new BufferedOutput(); + + $packagistRepo = $this->createMock(ComposerRepository::class); + $packagistRepo->method('getRepoConfig')->willReturn(['url' => 'https://repo.packagist.org']); + + $privatePackagistRepo = $this->createMock(ComposerRepository::class); + $privatePackagistRepo->method('getRepoConfig')->willReturn(['url' => 'https://repo.packagist.com/example']); + + $githubRepoDriver = $this->createMock(GitHubDriver::class); + $githubRepoDriver->method('getUrl')->willReturn('https://github.com/php/pie'); + + $vcsRepo = $this->createMock(VcsRepository::class); + $vcsRepo->method('getDriver')->willReturn($githubRepoDriver); + + $pathRepo = $this->createMock(PathRepository::class); + $pathRepo->method('getRepoConfig')->willReturn(['url' => '/path/to/repo']); + + $repoManager = $this->createMock(RepositoryManager::class); + $repoManager->method('getRepositories')->willReturn([ + $packagistRepo, + $privatePackagistRepo, + $vcsRepo, + $pathRepo, + ]); + + $composer = $this->createMock(Composer::class); + $composer->method('getRepositoryManager')->willReturn($repoManager); + + CommandHelper::listRepositories($composer, $output); + + self::assertSame( + str_replace("\r\n", "\n", <<<'OUTPUT' + The following repositories are in use for this Target PHP: + - Packagist + - Composer (https://repo.packagist.com/example) + - VCS Repository (https://github.com/php/pie) + - Path Repository (/path/to/repo) + OUTPUT), + str_replace("\r\n", "\n", trim($output->fetch())), + ); + } } diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php new file mode 100644 index 00000000..29edc044 --- /dev/null +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -0,0 +1,197 @@ +ensureExists(); + + self::assertFileExists($testPieJson); + self::assertSame( + $this->normaliseJson("{\n}\n"), + $this->normaliseJson(file_get_contents($testPieJson)), + ); + } + + public function testCanAddRequire(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson, dirname($testPieJson)); + $editor->ensureExists(); + + $editor->addRequire('foo/bar', '^1.2'); + self::assertSame( + $this->normaliseJson(<<<'EOF' + { + "require": { + "foo/bar": "^1.2" + } + } + EOF), + $this->normaliseJson(file_get_contents($testPieJson)), + ); + } + + public function testCanRevert(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson, dirname($testPieJson)); + $editor->ensureExists(); + $originalContent = $editor->addRequire('foo/bar', '^1.2'); + $editor->revert($originalContent); + self::assertSame( + $this->normaliseJson($originalContent), + $this->normaliseJson(file_get_contents($testPieJson)), + ); + } + + public function testCanAddAndRemoveRepositories(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson, dirname($testPieJson)); + $editor->ensureExists(); + + $originalContent = $editor->addRepository( + 'vcs', + 'https://github.com/php/pie', + ); + + self::assertSame( + $this->normaliseJson("{\n}\n"), + $this->normaliseJson($originalContent), + ); + + $expectedRepoContent = $this->normaliseJson(<<<'EOF' + { + "repositories": { + "https://github.com/php/pie": { + "type": "vcs", + "url": "https://github.com/php/pie" + } + } + } + EOF); + + self::assertSame( + $expectedRepoContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); + + $originalContent2 = $editor->removeRepository('https://github.com/php/pie'); + self::assertSame( + $expectedRepoContent, + $this->normaliseJson($originalContent2), + ); + + $noRepositoriesContent = $this->normaliseJson(<<<'EOF' + { + "repositories": { + } + } + EOF); + + self::assertSame( + $noRepositoriesContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); + + $originalContent3 = $editor->excludePackagistOrg(); + self::assertSame( + $noRepositoriesContent, + $this->normaliseJson($originalContent3), + ); + + self::assertSame( + $this->normaliseJson(<<<'EOF' + { + "repositories": { + "packagist.org": false + } + } + EOF), + $this->normaliseJson(file_get_contents($testPieJson)), + ); + } + + public function testCanAddAndRemoveWithTrailingSlash(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson, dirname($testPieJson)); + $editor->ensureExists(); + + $originalContent = $editor->addRepository( + 'path', + '/pwd/dummy/', + ); + + self::assertSame( + $this->normaliseJson("{\n}\n"), + $this->normaliseJson($originalContent), + ); + + $expectedRepoContent = $this->normaliseJson(<<<'EOF' + { + "repositories": { + "/pwd/dummy": { + "type": "path", + "url": "/pwd/dummy/" + } + } + } + EOF); + + self::assertSame( + $expectedRepoContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); + + $originalContent2 = $editor->removeRepository('/pwd/dummy/'); + self::assertSame( + $expectedRepoContent, + $this->normaliseJson($originalContent2), + ); + + $noRepositoriesContent = $this->normaliseJson(<<<'EOF' + { + "repositories": { + } + } + EOF); + + self::assertSame( + $noRepositoriesContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); + } + + private function normaliseJson(string $fileContent): string + { + return json_encode(json_decode($fileContent)); + } +}