diff --git a/README.UPGRADING.md b/README.UPGRADING.md index 65b6db82..a482333d 100644 --- a/README.UPGRADING.md +++ b/README.UPGRADING.md @@ -2,6 +2,6 @@ ## Upgrade Guide -### Upgrading to Version 1.0.0 +### Upgrading to Version 1.5 -TBD +You need to install `php-http/guzzle6-adapter` and ` php-http/message`, then use the libaray as normal. \ No newline at end of file diff --git a/README.md b/README.md index 172c983e..3656aa2d 100644 --- a/README.md +++ b/README.md @@ -249,9 +249,17 @@ $provider = new \League\OAuth2\Client\Provider\GenericProvider([ Via Composer ``` bash -$ composer require league/oauth2-client +$ composer require league/oauth2-client php-http/guzzle6-adapter php-http/message ``` +The two extra packages from `php-http` is part of the [HTTPlug](http://httplug.io/) organisation. It helps us not to +be coupled to Guzzle. That is why you need to install the `php-http/guzzle6-adapter` or any other library providing the virtual +[php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation) package. The +`php-http/message` package contains factory classes to create Guzzle and Diactoros PSR-7 requests. + +You can read more about HTTPlug and how to use it in +[their documentation](http://docs.php-http.org/en/latest/httplug/users.html). + ## Contributing Please see [CONTRIBUTING](https://github.com/thephpleague/oauth2-client/blob/master/CONTRIBUTING.md) for details. diff --git a/composer.json b/composer.json index 5f0a7e95..a626ad3e 100644 --- a/composer.json +++ b/composer.json @@ -4,15 +4,21 @@ "license": "MIT", "require": { "php": ">=5.5.0", + "psr/http-message": "^1.0", "ext-curl": "*", "ircmaxell/random-lib": "~1.1", - "guzzlehttp/guzzle": "~6.0" + "php-http/httplug": "^1.0", + "php-http/client-implementation": "^1.0", + "php-http/message-factory": "^1.0.2", + "php-http/discovery": "^1.0" }, "require-dev": { "phpunit/phpunit": "~4.0", "mockery/mockery": "~0.9", "squizlabs/php_codesniffer": "~2.0", "satooshi/php-coveralls": "0.6.*", + "php-http/message": "^1.0", + "php-http/guzzle6-adapter": "^1.0", "jakub-onderka/php-parallel-lint": "0.8.*" }, "keywords": [ diff --git a/src/Provider/AbstractProvider.php b/src/Provider/AbstractProvider.php index 4c80dfe9..5b1cfbdf 100644 --- a/src/Provider/AbstractProvider.php +++ b/src/Provider/AbstractProvider.php @@ -14,9 +14,11 @@ namespace League\OAuth2\Client\Provider; -use GuzzleHttp\Client as HttpClient; -use GuzzleHttp\ClientInterface as HttpClientInterface; -use GuzzleHttp\Exception\BadResponseException; +use GuzzleHttp\ClientInterface as GuzzleClientInterface; +use GuzzleHttp\Client as GuzzleClient; +use Http\Adapter\Guzzle6\Client as HttplugGuzzle6; +use Http\Client\HttpClient; +use Http\Discovery\HttpClientDiscovery; use League\OAuth2\Client\Grant\AbstractGrant; use League\OAuth2\Client\Grant\GrantFactory; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; @@ -85,7 +87,7 @@ abstract class AbstractProvider protected $requestFactory; /** - * @var HttpClientInterface + * @var \Http\Client\HttpClient */ protected $httpClient; @@ -94,6 +96,20 @@ abstract class AbstractProvider */ protected $randomFactory; + /** + * The options given to the Guzzle6 client + * @var array + * @deprecated These are to be removed in the next major release. + * These options are only used in getHttpClient to keep BC. + */ + private $guzzle6Options = []; + + /** + * @var GuzzleClientInterface + * @deprecated This client is only used in getHttpClient to keep BC. + */ + private $guzzleClient; + /** * Constructs an OAuth 2.0 service provider. * @@ -123,14 +139,7 @@ public function __construct(array $options = [], array $collaborators = []) } $this->setRequestFactory($collaborators['requestFactory']); - if (empty($collaborators['httpClient'])) { - $client_options = $this->getAllowedClientOptions($options); - - $collaborators['httpClient'] = new HttpClient( - array_intersect_key($options, array_flip($client_options)) - ); - } - $this->setHttpClient($collaborators['httpClient']); + $this->setHttpClient($this->getHttpClientFromOptions($options, $collaborators)); if (empty($collaborators['randomFactory'])) { $collaborators['randomFactory'] = new RandomFactory(); @@ -207,11 +216,44 @@ public function getRequestFactory() /** * Sets the HTTP client instance. * - * @param HttpClientInterface $client + * @param HttpClient $client * @return self */ - public function setHttpClient(HttpClientInterface $client) + public function setHttpClient($client) { + if ($client instanceof GuzzleClientInterface) { + @trigger_error( + sprintf( + 'Passing a "%s" to "%s::setHttpClient" is deprecated. Use a "Http\Client\HttpClient" instead.', + GuzzleClientInterface::class, + static::class + ), + E_USER_DEPRECATED + ); + if (!class_exists(HttplugGuzzle6::class)) { + throw new \RuntimeException( + sprintf( + 'You must install "php-http/guzzle6-adapter" to be able to pass a "%s" to "%s::setHttpClient".', + GuzzleClientInterface::class, + static::class + ) + ); + } + $client = new HttplugGuzzle6($client); + } + + if (!$client instanceof HttpClient) { + $type = is_object($client) ? get_class($client) : gettype($client); + throw new \RuntimeException( + sprintf( + 'First parameter to "%s::setHttpClient" was expected to be a "%s", you provided a "%s"', + static::class, + HttpClient::class, + $type + ) + ); + } + $this->httpClient = $client; return $this; @@ -220,13 +262,43 @@ public function setHttpClient(HttpClientInterface $client) /** * Returns the HTTP client instance. * - * @return HttpClientInterface + * @return HttpClient */ - public function getHttpClient() + public function getHttplugClient() { return $this->httpClient; } + /** + * Returns the HTTP client instance. + * + * @return GuzzleClientInterface + */ + public function getHttpClient() + { + @trigger_error( + sprintf( + 'Using "%s::getHttpClient" is deprecated in favor for "%s::getHttplugClient".', + static::class, + static::class + ), + E_USER_DEPRECATED + ); + + if ($this->guzzleClient !== null) { + return $this->guzzleClient; + } + + if (!class_exists(GuzzleClient::class)) { + throw new \RuntimeException( + 'You must install "php-http/guzzle6-adapter" to be able to use "%s::getHttplugClient".', + static::class + ); + } + + return new GuzzleClient($this->guzzle6Options); + } + /** * Sets the instance of the CSPRNG random generator factory. * @@ -621,12 +693,7 @@ protected function createRequest($method, $url, $token, array $options) */ protected function sendRequest(RequestInterface $request) { - try { - $response = $this->getHttpClient()->send($request); - } catch (BadResponseException $e) { - $response = $e->getResponse(); - } - return $response; + return $this->getHttplugClient()->sendRequest($request); } /** @@ -840,4 +907,62 @@ public function getHeaders($token = null) return $this->getDefaultHeaders(); } + + /** + * Get a HttpClient from constructor options + * + * @param array $options + * @param array $collaborators + * + * @return HttpClient + */ + private function getHttpClientFromOptions(array $options, array $collaborators) + { + if (!empty($collaborators['httplugClient'])) { + // Use provided client + return $collaborators['httplugClient']; + } + + if (empty($collaborators['httpClient'])) { + $client_options = $this->getAllowedClientOptions($options); + $guzzle6Options = array_intersect_key($options, array_flip($client_options)); + + if (empty($guzzle6Options)) { + return HttpClientDiscovery::find(); + } else { + @trigger_error( + 'Passing options to Guzzle6 client is deprecated. Use "httplugClient" instead', + E_USER_DEPRECATED + ); + if (!class_exists(HttplugGuzzle6::class)) { + throw new \RuntimeException( + 'You must install "php-http/guzzle6-adapter" to be able to pass options to the Guzzle6 client.' + ); + } + $this->guzzle6Options = $guzzle6Options; + + return HttplugGuzzle6::createWithConfig($guzzle6Options); + } + } + + @trigger_error('The "httpClient" option is deprecated. Use "httplugClient" instead', E_USER_DEPRECATED); + $client = $collaborators['httpClient']; + if (!$client instanceof GuzzleClientInterface) { + throw new \RuntimeException( + sprintf( + 'The value provided with option "HttpClient" must be instance of "%s".', + GuzzleClientInterface::class + ) + ); + } + + if (!class_exists(HttplugGuzzle6::class)) { + throw new \RuntimeException( + 'You must install "php-http/guzzle6-adapter" to pass a Guzzle6 client with the "httpClient" option.' + ); + } + + $this->guzzleClient = $client; + return new HttplugGuzzle6($client); + } } diff --git a/src/Tool/RequestFactory.php b/src/Tool/RequestFactory.php index 1af43429..89be44c3 100644 --- a/src/Tool/RequestFactory.php +++ b/src/Tool/RequestFactory.php @@ -14,7 +14,9 @@ namespace League\OAuth2\Client\Tool; -use GuzzleHttp\Psr7\Request; +use Http\Discovery\MessageFactoryDiscovery; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamInterface; /** * Used to produce PSR-7 Request instances. @@ -23,6 +25,11 @@ */ class RequestFactory { + /** + * @var \Http\Message\RequestFactory + */ + private $factory; + /** * Creates a PSR-7 Request instance. * @@ -32,7 +39,7 @@ class RequestFactory * @param string|resource|StreamInterface $body Message body. * @param string $version HTTP protocol version. * - * @return Request + * @return RequestInterface */ public function getRequest( $method, @@ -41,7 +48,11 @@ public function getRequest( $body = null, $version = '1.1' ) { - return new Request($method, $uri, $headers, $body, $version); + if (!$this->factory) { + $this->factory = MessageFactoryDiscovery::find(); + } + + return $this->factory->createRequest($method, $uri, $headers, $body, $version); } /** @@ -70,7 +81,7 @@ protected function parseOptions(array $options) * @param null|string $uri * @param array $options * - * @return Request + * @return RequestInterface */ public function getRequestWithOptions($method, $uri, array $options = []) { diff --git a/test/src/Grant/GrantTestCase.php b/test/src/Grant/GrantTestCase.php index 3ce91466..2bfaa853 100644 --- a/test/src/Grant/GrantTestCase.php +++ b/test/src/Grant/GrantTestCase.php @@ -2,7 +2,7 @@ namespace League\OAuth2\Client\Test\Grant; -use GuzzleHttp\ClientInterface; +use Http\Client\HttpClient; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use League\OAuth2\Client\Token\AccessToken; @@ -44,7 +44,7 @@ abstract public function providerGetAccessToken(); /** * Callback to test access token request parameters. * - * @return Closure + * @return \Closure */ abstract protected function getParamExpectation(); @@ -64,8 +64,8 @@ public function testGetAccessToken($grant, array $params = []) $paramCheck = $this->getParamExpectation(); - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with( + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->with( $request = m::on(function ($request) use ($paramCheck) { parse_str((string) $request->getBody(), $body); return $paramCheck($body); diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php index a68cac50..c267273f 100644 --- a/test/src/Provider/AbstractProviderTest.php +++ b/test/src/Provider/AbstractProviderTest.php @@ -2,6 +2,8 @@ namespace League\OAuth2\Client\Test\Provider; +use GuzzleHttp\Client as GuzzleClient; +use Http\Client\HttpClient; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Test\Provider\Fake as MockProvider; use League\OAuth2\Client\Grant\AbstractGrant; @@ -14,7 +16,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; -use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\ClientInterface; use Mockery as m; @@ -105,8 +106,11 @@ public function testConstructorSetsClientOptions() $mockProvider = new MockProvider(compact('timeout')); - $config = $mockProvider->getHttpClient()->getConfig(); + $config = $this->getGuzzle6Config($mockProvider->getHttplugClient()); + $this->assertContains('timeout', $config); + $this->assertEquals($timeout, $config['timeout']); + $config = $mockProvider->getHttpClient()->getConfig(); $this->assertContains('timeout', $config); $this->assertEquals($timeout, $config['timeout']); } @@ -117,8 +121,11 @@ public function testCanSetAProxy() $mockProvider = new MockProvider(['proxy' => $proxy]); - $config = $mockProvider->getHttpClient()->getConfig(); + $config = $this->getGuzzle6Config($mockProvider->getHttplugClient()); + $this->assertContains('proxy', $config); + $this->assertEquals($proxy, $config['proxy']); + $config = $mockProvider->getHttpClient()->getConfig(); $this->assertContains('proxy', $config); $this->assertEquals($proxy, $config['proxy']); } @@ -127,8 +134,11 @@ public function testCannotDisableVerifyIfNoProxy() { $mockProvider = new MockProvider(['verify' => false]); - $config = $mockProvider->getHttpClient()->getConfig(); + $config = $this->getGuzzle6Config($mockProvider->getHttplugClient()); + $this->assertContains('verify', $config); + $this->assertTrue($config['verify']); + $config = $mockProvider->getHttpClient()->getConfig(); $this->assertContains('verify', $config); $this->assertTrue($config['verify']); } @@ -137,8 +147,11 @@ public function testCanDisableVerificationIfThereIsAProxy() { $mockProvider = new MockProvider(['proxy' => '192.168.0.1:8888', 'verify' => false]); - $config = $mockProvider->getHttpClient()->getConfig(); + $config = $this->getGuzzle6Config($mockProvider->getHttplugClient()); + $this->assertContains('verify', $config); + $this->assertFalse($config['verify']); + $config = $mockProvider->getHttpClient()->getConfig(); $this->assertContains('verify', $config); $this->assertFalse($config['verify']); } @@ -215,8 +228,8 @@ public function testGetUserProperties($response, $name = null, $email = null, $i $url = $provider->getResourceOwnerDetailsUrl($token); - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with( + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->with( m::on(function ($request) use ($url) { return $request->getMethod() === 'GET' && $request->hasHeader('Authorization') @@ -350,8 +363,8 @@ public function testErrorResponsesCanBeCustomizedAtTheProvider() $method = $provider->getAccessTokenMethod(); $url = $provider->getBaseAccessTokenUrl([]); - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with( + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->with( m::on(function ($request) use ($method, $url) { return $request->getMethod() === $method && (string) $request->getUri() === $url; @@ -392,29 +405,12 @@ public function testClientErrorTriggersProviderException() '{"error":"Foo error","code":1337}' ); - $request = m::mock(RequestInterface::class); - $response = m::mock(ResponseInterface::class); - $response->shouldReceive('getStatusCode')->times(1)->andReturn(400); $response->shouldReceive('getBody')->times(1)->andReturn($stream); $response->shouldReceive('getHeader')->with('content-type')->andReturn('application/json'); - $exception = new BadResponseException( - 'test exception', - $request, - $response - ); - - $method = $provider->getAccessTokenMethod(); - $url = $provider->getBaseAccessTokenUrl([]); - - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with( - m::on(function ($request) use ($method, $url) { - return $request->getMethod() === $method - && (string) $request->getUri() === $url; - }) - )->times(1)->andThrow($exception); + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->times(1)->andReturn($response); $provider->setHttpClient($client); $provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); @@ -442,8 +438,8 @@ public function testAuthenticatedRequestAndResponse() $response->shouldReceive('getBody')->times(1)->andReturn($stream); $response->shouldReceive('getHeader')->with('content-type')->times(1)->andReturn('application/json'); - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with($request)->andReturn($response); + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->with($request)->andReturn($response); $provider->setHttpClient($client); @@ -501,8 +497,8 @@ public function testGetAccessToken($method) $method = $provider->getAccessTokenMethod(); $url = $provider->getBaseAccessTokenUrl([]); - $client = m::mock(ClientInterface::class); - $client->shouldReceive('send')->with( + $client = m::mock(HttpClient::class); + $client->shouldReceive('sendRequest')->with( m::on(function ($request) use ($method, $url) { return $request->getMethod() === $method && (string) $request->getUri() === $url; @@ -670,4 +666,62 @@ public function testDefaultAuthorizationHeaders() $this->assertEquals([], $headers); } + + public function testSetHttpClientWithGuzzleClient() + { + $provider = new MockProvider(); + $provider->setHttpClient(new GuzzleClient([])); + } + + /** + * @expectedException \RuntimeException + */ + public function testSetHttpClientWithBogusData() + { + $provider = new MockProvider(); + $provider->setHttpClient('foo'); + } + + public function testGetHttpClientFromOptions() + { + $provider = new MockProvider(); + $method = new \ReflectionMethod($provider, 'getHttpClientFromOptions'); + $method->setAccessible(true); + + $expected = 'foo'; + $result = $method->invoke($provider, [], ['httplugClient'=> $expected]); + $this->assertEquals($expected, $result); + } + + /** + * @expectedException \RuntimeException + */ + public function testGetHttpClientFromOptionsWithBogusData() + { + $provider = new MockProvider(); + $method = new \ReflectionMethod($provider, 'getHttpClientFromOptions'); + $method->setAccessible(true); + + $method->invoke($provider, [], ['httpClient'=> 'foo']); + } + + /** + * Helper method to get config from Guzzle6. + * + * @param HttpClient $httplugGuzzle6 + * + * @return array + */ + private function getGuzzle6Config(HttpClient $httplugGuzzle6) + { + $reflectionClass = new \ReflectionClass($httplugGuzzle6); + $reflectionProperty = $reflectionClass->getProperty('client'); + $reflectionProperty->setAccessible(true); + + $reflectionGuzzle = $reflectionProperty->getValue($httplugGuzzle6); + + $config = $reflectionGuzzle->getConfig(); + + return $config; + } }