Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow pinning the preferred implementations in composer.json #232

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";