Skip to content

Commit 9a9e049

Browse files
committed
Support Both HTTPlug's RequestFactory and PSR's RequestFactoryInterface
1 parent 0b80607 commit 9a9e049

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
"require": {
1414
"php": "^7.2 || ^8.0",
1515
"ext-json": "*",
16+
"composer-runtime-api": "^2.0",
1617
"php-http/client-common": "^1.9 || ^2.0",
1718
"php-http/discovery": "^1.0",
1819
"psr/http-client-implementation": "^1.0",
1920
"php-http/message-factory": "^1.0",
2021
"psr/http-message-implementation": "^1.0",
21-
"composer-runtime-api": "^2.0"
22+
"symfony/deprecation-contracts": "^2.5 || ^3.0"
2223
},
2324
"require-dev": {
2425
"friendsofphp/php-cs-fixer": "^3.0",

src/HttpClient/HttpPluginClientBuilder.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Http\Client\Common\PluginClient;
1515
use Http\Discovery\Psr17FactoryDiscovery;
1616
use Http\Discovery\Psr18ClientDiscovery;
17+
use Http\Message\RequestFactory;
1718
use Psr\Http\Client\ClientInterface;
1819
use Psr\Http\Message\RequestFactoryInterface;
1920

@@ -28,10 +29,33 @@ class HttpPluginClientBuilder
2829
/** @var Plugin[] */
2930
private $plugins = [];
3031

31-
public function __construct(ClientInterface $httpClient = null, RequestFactoryInterface $requestFactory = null)
32+
/**
33+
* @param ClientInterface|null $httpClient
34+
* @param RequestFactory|RequestFactoryInterface|null $requestFactory
35+
*/
36+
public function __construct(ClientInterface $httpClient = null, $requestFactory = null)
3237
{
38+
$requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
39+
if ($requestFactory instanceof RequestFactory) {
40+
trigger_deprecation(
41+
'private-packagist/api-client',
42+
'1.35.0',
43+
'',
44+
RequestFactory::class,
45+
RequestFactoryInterface::class
46+
);
47+
} elseif (!$requestFactory instanceof RequestFactoryInterface) {
48+
throw new \TypeError(sprintf(
49+
'%s::__construct(): Argument #2 ($requestFactory) must be of type %s|%s, %s given',
50+
self::class,
51+
RequestFactory::class,
52+
RequestFactoryInterface::class,
53+
is_object($requestFactory) ? get_class($requestFactory) : gettype($requestFactory)
54+
));
55+
}
56+
3357
$this->httpClient = $httpClient ?: Psr18ClientDiscovery::find();
34-
$this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
58+
$this->requestFactory = $requestFactory;
3559
}
3660

3761
public function addPlugin(Plugin $plugin)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* (c) Packagist Conductors GmbH <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace PrivatePackagist\ApiClient\HttpClient\Plugin;
11+
12+
use GuzzleHttp\Psr7\HttpFactory;
13+
use GuzzleHttp\Psr7\Response;
14+
use Http\Client\Common\HttpMethodsClientInterface;
15+
use Http\Message\MessageFactory\GuzzleMessageFactory;
16+
use Http\Message\RequestMatcher as RequestMatcherInterface;
17+
use Http\Mock\Client as MockClient;
18+
use PHPUnit\Framework\TestCase;
19+
use PrivatePackagist\ApiClient\HttpClient\HttpPluginClientBuilder;
20+
use Psr\Http\Message\RequestInterface;
21+
use Psr\Http\Message\ResponseInterface;
22+
23+
class HttpPluginClientBuilderTest extends TestCase
24+
{
25+
/** @dataProvider provideRequestFactories */
26+
public function testRequestFactory(?object $factory, ?string $expectedException): void
27+
{
28+
if ($expectedException !== null) {
29+
$this->expectException($expectedException);
30+
}
31+
32+
$mockHttp = new MockClient;
33+
$mockHttp->setDefaultException(new \Exception('Mock HTTP client did not match request.'));
34+
$mockHttp->on($this->matchRequestIncludingHeaders(), new Response(307, ['Location' => '/kittens.jpg']));
35+
36+
$builder = new HttpPluginClientBuilder($mockHttp, $factory);
37+
// Make sure that the RequestFactory passed is acceptable for the client.
38+
$client = $builder->getHttpClient();
39+
$this->assertInstanceOf(HttpMethodsClientInterface::class, $client);
40+
41+
// Ensure that the Request Factory correctly generates a request object (including headers
42+
// as RequestFactory and RequestFactoryInterface set headers differently).
43+
$response = $client->get('https://example.com/puppies.jpg', ['Accept' => 'image/vnd.cute+jpeg']);
44+
$this->assertInstanceOf(ResponseInterface::class, $response);
45+
$this->assertSame(307, $response->getStatusCode());
46+
$locationHeaders = $response->getHeader('Location');
47+
$this->assertCount(1, $locationHeaders);
48+
$this->assertSame('/kittens.jpg', reset($locationHeaders));
49+
}
50+
51+
/**
52+
* The concrete implementation of the RequestMatcher interface does not allow matching on
53+
* headers, which we need to test to ensure both legacy and PSR17 implementations work.
54+
*/
55+
protected function matchRequestIncludingHeaders(): RequestMatcherInterface
56+
{
57+
return new class implements RequestMatcherInterface {
58+
public function matches(RequestInterface $request): bool
59+
{
60+
$acceptHeaders = $request->getHeader('Accept');
61+
return $request->getUri()->getPath() === '/puppies.jpg'
62+
&& count($acceptHeaders) === 1
63+
&& reset($acceptHeaders) === 'image/vnd.cute+jpeg';
64+
}
65+
};
66+
}
67+
68+
/** @return iterable{object|null, class-string|null} */
69+
private static function provideRequestFactories(): iterable
70+
{
71+
// Fallback
72+
yield [null, null];
73+
// Legacy
74+
yield [new GuzzleMessageFactory, null];
75+
// PSR17
76+
yield [new HttpFactory, null];
77+
// Invalid
78+
yield [new \stdClass, \TypeError::class];
79+
}
80+
}

0 commit comments

Comments
 (0)