Skip to content

Commit

Permalink
Allow pinning the preferred implementations in composer.json (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas authored Apr 26, 2023
1 parent 07b7cc3 commit 786d27f
Show file tree
Hide file tree
Showing 15 changed files with 205 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/installation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 14 additions & 5 deletions .github/workflows/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,28 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
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) ]
2 changes: 1 addition & 1 deletion .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
"autoload": {
"psr-4": {
"Http\\Discovery\\": "src/"
}
},
"exclude-from-classmap": [
"src/Composer/Plugin.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
10 changes: 9 additions & 1 deletion src/ClassDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ abstract class ClassDiscovery
* @var array
*/
private static $strategies = [
Strategy\GeneratedDiscoveryStrategy::class,
Strategy\CommonClassesStrategy::class,
Strategy\CommonPsr17ClassesStrategy::class,
Strategy\PuliBetaStrategy::class,
Expand Down Expand Up @@ -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;
Expand Down
86 changes: 86 additions & 0 deletions src/Composer/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Composer\Repository\RepositorySet;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;
use Http\Discovery\ClassDiscovery;

/**
Expand Down Expand Up @@ -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',
];
}
Expand Down Expand Up @@ -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 = <<<EOPHP
<?php
namespace Http\Discovery\Strategy;
class GeneratedDiscoveryStrategy implements DiscoveryStrategy
{
public static function getCandidates(\$type)
{
switch (\$type) {
$candidates
default: return [];
}
}
}
EOPHP
;

if (!file_exists($file) || $code !== file_get_contents($file)) {
file_put_contents($file, $code);
}

$rootPackage = $event->getComposer()->getPackage();
$autoload = $rootPackage->getAutoload();
$autoload['classmap'][] = $vendorDir.'/composer/GeneratedDiscoveryStrategy.php';
$rootPackage->setAutoload($autoload);
}

private function updateComposerJson(array $missingRequires, bool $sortPackages)
{
$file = Factory::getComposerFile();
Expand Down
4 changes: 3 additions & 1 deletion tests/Composer/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
4 changes: 2 additions & 2 deletions tests/plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/vendor
/composer.lock
vendor
composer.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"repositories": [
{
"type": "path",
"url": "../..",
"url": "../../..",
"options": {
"versions": {
"php-http/discovery": "99.99.x-dev"
Expand Down
28 changes: 28 additions & 0 deletions tests/plugin/pinning/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
7 changes: 7 additions & 0 deletions tests/plugin/pinning/test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use Http\Discovery\Psr17FactoryDiscovery;

require __DIR__.'/vendor/autoload.php';

echo get_class(Psr17FactoryDiscovery::findRequestFactory())."\n";

0 comments on commit 786d27f

Please sign in to comment.