diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index f6d7b23..2e96d6b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Roave BC Check uses: "docker://nyholm/roave-bc-check-ga" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b8230..6d534b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/installation.yml b/.github/workflows/installation.yml index a9e49ee..d09fe6d 100644 --- a/.github/workflows/installation.yml +++ b/.github/workflows/installation.yml @@ -79,7 +79,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/plugin.yml b/.github/workflows/plugin.yml index 9624c0d..1ad9e1f 100644 --- a/.github/workflows/plugin.yml +++ b/.github/workflows/plugin.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -26,11 +26,20 @@ jobs: php-version: 7.1 tools: composer:${{ matrix.composer }} - - name: Check Plugin + - name: Check Auto-install run: | - mkdir /tmp/plugin + mkdir /tmp/plugin-auto-install # replace the relative path for the repository url with an absolute path for composer v1 compatibility - jq '.repositories[0].url="'$(pwd)'"' tests/plugin/composer.json > /tmp/plugin/composer.json - cd /tmp/plugin + jq '.repositories[0].url="'$(pwd)'"' tests/plugin/auto-install/composer.json > /tmp/plugin-auto-install/composer.json + cd /tmp/plugin-auto-install composer update composer show http-interop/http-factory-guzzle -q + + - name: Check Pinning + run: | + cp -a tests/plugin/pinning /tmp/plugin-pinning + # replace the relative path for the repository url with an absolute path for composer v1 compatibility + jq '.repositories[0].url="'$(pwd)'"' tests/plugin/pinning/composer.json > /tmp/plugin-pinning/composer.json + cd /tmp/plugin-pinning + composer update + [ 'Slim\Psr7\Factory\RequestFactory' == $(php test.php) ] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index cabdb5d..dcf840d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: PHP-CS-Fixer uses: docker://oskarstark/php-cs-fixer-ga diff --git a/CHANGELOG.md b/CHANGELOG.md index 476d649..1219a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.17.0 - 2023-XX-XX - [#230](https://github.com/php-http/discovery/pull/230) - Add Psr18Client to make it straightforward to use PSR-18 +- [#232](https://github.com/php-http/discovery/pull/232) - Allow pinning the preferred implementations in composer.json ## 1.16.0 - 2023-04-26 diff --git a/README.md b/README.md index 51708e7..1d9fd99 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,15 @@ composer require php-http/discovery ``` -## Usage +## Usage as a library author Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.html). If your library/SDK needs a PSR-18 client, here is a quick example. -First, you need to install a PSR-18 client and a PSR-17 factory implementations. This should -be done only for dev dependencies as you don't want to force a specific one on your users: +First, you need to install a PSR-18 client and a PSR-17 factory implementations. +This should be done only for dev dependencies as you don't want to force a +specific implementation on your users: ```bash composer require --dev symfony/http-client @@ -40,8 +41,8 @@ because you just installed the dev dependencies you need for testing: composer config allow-plugins.php-http/discovery false ``` -Finally, you need to require `php-http/discovery` and the generic implementations that -your library is going to need: +Finally, you need to require `php-http/discovery` and the generic implementations +that your library is going to need: ```bash composer require php-http/discovery:^1.17 @@ -60,7 +61,44 @@ $request = $client->createRequest('GET', 'https://example.com'); $response = $client->sendRequest($request); ``` -Internally, this code will use whatever PSR-7, PSR-17 and PSR-18 implementations that your users have installed. +Internally, this code will use whatever PSR-7, PSR-17 and PSR-18 implementations +that your users have installed. + + +## Usage as a library user + +If you use a library/SDK that requires `php-http/discovery`, you can configure +the auto-discovery mechanism to use a specific implementation when many are +available in your project. + +For example, if you have both `nyholm/psr7` and `guzzlehttp/guzzle` in your +project, you can tell `php-http/discovery` to use `guzzlehttp/guzzle` instead of +`nyholm/psr7` by running the following command: + +```bash +composer config extra.discovery.psr/http-factory-implementation GuzzleHttp\\Psr7\\HttpFactory +``` + +This will update your `composer.json` file to add the following configuration: + +```json +{ + "extra": { + "discovery": { + "psr/http-factory-implementation": "GuzzleHttp\\Psr7\\HttpFactory" + } + } +} +``` + +Don't forget to run `composer install` to apply the changes, and ensure that +the composer plugin is enabled: + +```bash +composer config allow-plugins.php-http/discovery true +composer install +``` + ## Testing diff --git a/composer.json b/composer.json index ec9eb4f..d38ab83 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,10 @@ "autoload": { "psr-4": { "Http\\Discovery\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/ClassDiscovery.php b/src/ClassDiscovery.php index 4f47f3c..60e3734 100644 --- a/src/ClassDiscovery.php +++ b/src/ClassDiscovery.php @@ -22,6 +22,7 @@ abstract class ClassDiscovery * @var array */ private static $strategies = [ + Strategy\GeneratedDiscoveryStrategy::class, Strategy\CommonClassesStrategy::class, Strategy\CommonPsr17ClassesStrategy::class, Strategy\PuliBetaStrategy::class, @@ -54,10 +55,17 @@ protected static function findOneByType($type) return $class; } + static $skipStrategy; + $skipStrategy ?? $skipStrategy = self::safeClassExists(Strategy\GeneratedDiscoveryStrategy::class) ? false : Strategy\GeneratedDiscoveryStrategy::class; + $exceptions = []; foreach (self::$strategies as $strategy) { + if ($skipStrategy === $strategy) { + continue; + } + try { - $candidates = call_user_func($strategy.'::getCandidates', $type); + $candidates = $strategy::getCandidates($type); } catch (StrategyUnavailableException $e) { if (!isset(self::$deprecatedStrategies[$strategy])) { $exceptions[] = $e; diff --git a/src/Composer/Plugin.php b/src/Composer/Plugin.php index ba79838..ea5df49 100644 --- a/src/Composer/Plugin.php +++ b/src/Composer/Plugin.php @@ -18,6 +18,7 @@ use Composer\Repository\RepositorySet; use Composer\Script\Event; use Composer\Script\ScriptEvents; +use Composer\Util\Filesystem; use Http\Discovery\ClassDiscovery; /** @@ -98,9 +99,30 @@ class Plugin implements PluginInterface, EventSubscriberInterface 'http-interop/http-factory-slim' => 'slim/slim:^3', ]; + private const INTERFACE_MAP = [ + 'php-http/async-client-implementation' => [ + 'Http\Client\HttpAsyncClient', + ], + 'php-http/client-implementation' => [ + 'Http\Client\HttpClient', + ], + 'psr/http-client-implementation' => [ + 'Psr\Http\Client\ClientInterface', + ], + 'psr/http-factory-implementation' => [ + 'Psr\Http\Message\RequestFactoryInterface', + 'Psr\Http\Message\ResponseFactoryInterface', + 'Psr\Http\Message\ServerRequestFactoryInterface', + 'Psr\Http\Message\StreamFactoryInterface', + 'Psr\Http\Message\UploadedFileFactoryInterface', + 'Psr\Http\Message\UriFactoryInterface', + ], + ]; + public static function getSubscribedEvents(): array { return [ + ScriptEvents::PRE_AUTOLOAD_DUMP => 'preAutoloadDump', ScriptEvents::POST_UPDATE_CMD => 'postUpdate', ]; } @@ -334,6 +356,70 @@ public function getMissingRequires(InstalledRepositoryInterface $repo, array $re return $missingRequires; } + public function preAutoloadDump(Event $event) + { + $filesystem = new Filesystem(); + // Double realpath() on purpose, see https://bugs.php.net/72738 + $vendorDir = $filesystem->normalizePath(realpath(realpath($event->getComposer()->getConfig()->get('vendor-dir')))); + $filesystem->ensureDirectoryExists($vendorDir.'/composer'); + $pinned = $event->getComposer()->getPackage()->getExtra()['discovery'] ?? []; + $candidates = []; + + $allInterfaces = array_merge(...array_values(self::INTERFACE_MAP)); + foreach ($pinned as $abstraction => $class) { + if (isset(self::INTERFACE_MAP[$abstraction])) { + $interfaces = self::INTERFACE_MAP[$abstraction]; + } elseif (false !== $k = array_search($abstraction, $allInterfaces, true)) { + $interfaces = [$allInterfaces[$k]]; + } else { + throw new \UnexpectedValueException(sprintf('Invalid "extra.discovery" pinned in composer.json: "%s" is not one of ["%s"].', $abstraction, implode('", "', array_keys(self::INTERFACE_MAP)))); + } + + foreach ($interfaces as $interface) { + $candidates[] = sprintf("case %s: return [['class' => %s]];\n", var_export($interface, true), var_export($class, true)); + } + } + + $file = $vendorDir.'/composer/GeneratedDiscoveryStrategy.php'; + + if (!$candidates) { + if (file_exists($file)) { + unlink($file); + } + + return; + } + + $candidates = implode(' ', $candidates); + $code = <<getComposer()->getPackage(); + $autoload = $rootPackage->getAutoload(); + $autoload['classmap'][] = $vendorDir.'/composer/GeneratedDiscoveryStrategy.php'; + $rootPackage->setAutoload($autoload); + } + private function updateComposerJson(array $missingRequires, bool $sortPackages) { $file = Factory::getComposerFile(); diff --git a/tests/Composer/PluginTest.php b/tests/Composer/PluginTest.php index bf4fa67..6ab24d0 100644 --- a/tests/Composer/PluginTest.php +++ b/tests/Composer/PluginTest.php @@ -81,7 +81,9 @@ public static function provideMissingRequires() yield 'move-to-require' => [$expected, $repo, $rootRequires, []]; $package = new Package('symfony/symfony', '1.0.0.0', '1.0'); - $package->setReplaces([new Link('symfony/symfony', 'symfony/http-client', new Constraint(Constraint::STR_OP_GE, '1'))]); + $package->setReplaces([ + 'symfony/http-client' => new Link('symfony/symfony', 'symfony/http-client', new Constraint(Constraint::STR_OP_GE, '1')) + ]); $repo = new InstalledArrayRepository([ 'php-http/discovery' => new Package('php-http/discovery', '1.0.0.0', '1.0'), diff --git a/tests/plugin/.gitignore b/tests/plugin/.gitignore index de4a392..7579f74 100644 --- a/tests/plugin/.gitignore +++ b/tests/plugin/.gitignore @@ -1,2 +1,2 @@ -/vendor -/composer.lock +vendor +composer.lock diff --git a/tests/plugin/composer.json b/tests/plugin/auto-install/composer.json similarity index 93% rename from tests/plugin/composer.json rename to tests/plugin/auto-install/composer.json index 4554516..0b395f2 100644 --- a/tests/plugin/composer.json +++ b/tests/plugin/auto-install/composer.json @@ -2,7 +2,7 @@ "repositories": [ { "type": "path", - "url": "../..", + "url": "../../..", "options": { "versions": { "php-http/discovery": "99.99.x-dev" diff --git a/tests/plugin/pinning/composer.json b/tests/plugin/pinning/composer.json new file mode 100644 index 0000000..4ce129b --- /dev/null +++ b/tests/plugin/pinning/composer.json @@ -0,0 +1,28 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../..", + "options": { + "versions": { + "php-http/discovery": "99.99.x-dev" + } + } + } + ], + "require": { + "nyholm/psr7": "*", + "php-http/discovery": "99.99.x-dev", + "slim/psr7": "*" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } + }, + "extra": { + "discovery": { + "Psr\\Http\\Message\\RequestFactoryInterface": "Slim\\Psr7\\Factory\\RequestFactory" + } + } +} diff --git a/tests/plugin/pinning/test.php b/tests/plugin/pinning/test.php new file mode 100644 index 0000000..f91883a --- /dev/null +++ b/tests/plugin/pinning/test.php @@ -0,0 +1,7 @@ +