Skip to content

Commit

Permalink
Merge pull request #67 from packagist/z/httplug-upgrade
Browse files Browse the repository at this point in the history
Upgrade HTTPlug from Legacy to PSR 17/18 Discovery Factories
  • Loading branch information
glaubinix committed Oct 10, 2023
2 parents 98aa7dd + 9427e98 commit 2fa51cf
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 19 deletions.
16 changes: 8 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
],
"require": {
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.0",
"php-http/httplug": "^1.1 || ^2.0",
"ext-json": "*",
"composer-runtime-api": "^2.0",
"php-http/client-common": "^1.9 || ^2.0",
"php-http/discovery": "^1.0",
"psr/http-client-implementation": "^1.0",
"php-http/client-common": "^1.9 || ^2.0",
"php-http/message-factory": "^1.0",
"composer-runtime-api": "^2.0"
"psr/http-message-implementation": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0 || ^9.0",
"guzzlehttp/guzzle": "^7",
"php-http/mock-client": "^1.0",
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^1.2"
"guzzlehttp/guzzle": "^7.0",
"php-http/mock-client": "^1.0",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"autoload": {
"psr-4": { "PrivatePackagist\\ApiClient\\": "src/" }
Expand Down
4 changes: 2 additions & 2 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace PrivatePackagist\ApiClient;

use Http\Client\Common\Plugin;
use Http\Discovery\UriFactoryDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use PrivatePackagist\ApiClient\HttpClient\HttpPluginClientBuilder;
use PrivatePackagist\ApiClient\HttpClient\Message\ResponseMediator;
use PrivatePackagist\ApiClient\HttpClient\Plugin\ExceptionThrower;
Expand All @@ -31,7 +31,7 @@ public function __construct(HttpPluginClientBuilder $httpClientBuilder = null, $
$privatePackagistUrl = $privatePackagistUrl ? : 'https://packagist.com';
$this->responseMediator = $responseMediator ? : new ResponseMediator();

$builder->addPlugin(new Plugin\AddHostPlugin(UriFactoryDiscovery::find()->createUri($privatePackagistUrl)));
$builder->addPlugin(new Plugin\AddHostPlugin(Psr17FactoryDiscovery::findUriFactory()->createUri($privatePackagistUrl)));
$builder->addPlugin(new PathPrepend('/api'));
$builder->addPlugin(new Plugin\RedirectPlugin());
$headers = [
Expand Down
52 changes: 43 additions & 9 deletions src/HttpClient/HttpPluginClientBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,59 @@
use Http\Client\Common\HttpMethodsClient;
use Http\Client\Common\Plugin;
use Http\Client\Common\PluginClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

class HttpPluginClientBuilder
{
/** @var ClientInterface */
private $httpClient;
/** @var HttpMethodsClient|null */
private $pluginClient;
/** @var RequestFactory */
/** @var RequestFactory|RequestFactoryInterface */
private $requestFactory;
/** @var StreamFactoryInterface */
private $streamFactory;
/** @var Plugin[] */
private $plugins = [];

public function __construct(ClientInterface $httpClient = null, RequestFactory $requestFactory = null)
{
$this->httpClient = $httpClient ?: HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find();
/**
* @param RequestFactory|RequestFactoryInterface|null $requestFactory
* @param StreamFactoryInterface|null $streamFactory
*/
public function __construct(
?ClientInterface $httpClient = null,
$requestFactory = null,
?StreamFactoryInterface $streamFactory= null
) {
$requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
if ($requestFactory instanceof RequestFactory) {
// Use same format as symfony/deprecation-contracts.
@trigger_error(sprintf(
'Since %s %s: %s is deprecated, use %s instead.',
'private-packagist/api-client',
'1.36.0',
RequestFactory::class,
RequestFactoryInterface::class
), \E_USER_DEPRECATED);
} elseif (!$requestFactory instanceof RequestFactoryInterface) {
/** @var mixed $requestFactory value unknown; set to mixed, prevent PHPStan complaining about guard clauses */
throw new \TypeError(sprintf(
'%s::__construct(): Argument #2 ($requestFactory) must be of type %s|%s, %s given',
self::class,
RequestFactory::class,
RequestFactoryInterface::class,
is_object($requestFactory) ? get_class($requestFactory) : gettype($requestFactory)
));
}

$this->httpClient = $httpClient ?? Psr18ClientDiscovery::find();
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
}

public function addPlugin(Plugin $plugin)
Expand All @@ -41,7 +74,7 @@ public function addPlugin(Plugin $plugin)
}

/**
* @param string $pluginClass
* @param class-string $pluginClass
*/
public function removePlugin($pluginClass)
{
Expand All @@ -58,7 +91,8 @@ public function getHttpClient()
if (!$this->pluginClient) {
$this->pluginClient = new HttpMethodsClient(
new PluginClient($this->httpClient, $this->plugins),
$this->requestFactory
$this->requestFactory,
$this->streamFactory
);
}

Expand Down
81 changes: 81 additions & 0 deletions tests/HttpClient/HttpPluginClientBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/*
* (c) Packagist Conductors GmbH <contact@packagist.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace PrivatePackagist\ApiClient\HttpClient\Plugin;

use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Psr7\Response;
use Http\Client\Common\HttpMethodsClientInterface;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Http\Message\RequestMatcher as RequestMatcherInterface;
use Http\Mock\Client as MockClient;
use PHPUnit\Framework\TestCase;
use PrivatePackagist\ApiClient\HttpClient\HttpPluginClientBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class HttpPluginClientBuilderTest extends TestCase
{
public function testInvalidRequestFactory(): void
{
$this->expectException(\TypeError::class);
$definitelyNotARequestFactory = new \stdClass;
/** @phpstan-ignore-next-line We are passing in an invalid type on purpose. */
new HttpPluginClientBuilder(new MockClient, $definitelyNotARequestFactory);
}

/** @dataProvider provideRequestFactories */
public function testRequestFactory(?object $factory): void
{
$mockHttp = new MockClient;
$mockHttp->setDefaultException(new \Exception('Mock HTTP client did not match request.'));
$mockHttp->on($this->matchRequestIncludingHeaders(), new Response(307, ['Location' => '/kittens.jpg']));

$builder = new HttpPluginClientBuilder($mockHttp, $factory);
// Make sure that the RequestFactory passed is acceptable for the client.
$client = $builder->getHttpClient();
$this->assertInstanceOf(HttpMethodsClientInterface::class, $client);

// Ensure that the Request Factory correctly generates a request object (including headers
// as RequestFactory and RequestFactoryInterface set headers differently).
$response = $client->get('https://example.com/puppies.jpg', ['Accept' => 'image/vnd.cute+jpeg']);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertSame(307, $response->getStatusCode());
$locationHeaders = $response->getHeader('Location');
$this->assertCount(1, $locationHeaders);
$this->assertSame('/kittens.jpg', reset($locationHeaders));
}

/**
* The concrete implementation of the RequestMatcher interface does not allow matching on
* headers, which we need to test to ensure both legacy and PSR17 implementations work.
*/
private function matchRequestIncludingHeaders(): RequestMatcherInterface
{
return new class implements RequestMatcherInterface {
public function matches(RequestInterface $request): bool
{
$acceptHeaders = $request->getHeader('Accept');
return $request->getUri()->getPath() === '/puppies.jpg'
&& count($acceptHeaders) === 1
&& reset($acceptHeaders) === 'image/vnd.cute+jpeg';
}
};
}

/** @return iterable{object|null} */
public static function provideRequestFactories(): iterable
{
yield [null];
// Http\Message\RequestFactory
yield [new GuzzleMessageFactory];
// Psr\Http\Message\RequestFactoryInterface
yield [new HttpFactory];
}
}

0 comments on commit 2fa51cf

Please sign in to comment.