diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 765e416..8a52cbd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,14 +3,14 @@ on: push: pull_request: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: true matrix: - php: [7.4, 8.0, 8.1] + php: [7.4, 8.0, 8.1, 8.2, 8.3] name: PHP - ${{ matrix.php }} steps: - name: Checkout code @@ -30,4 +30,4 @@ jobs: composer require "phpstan/phpstan:0.12.99" vendor/bin/phpstan analyse --no-progress - name: Execute tests - run: vendor/bin/phpunit --verbose + run: vendor/bin/phpunit diff --git a/.phpstan.ignoreErrors.neon b/.phpstan.ignoreErrors.neon deleted file mode 100644 index ada8f6e..0000000 --- a/.phpstan.ignoreErrors.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - ignoreErrors: [] \ No newline at end of file diff --git a/README.md b/README.md index d0e856b..27e49d0 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # Mollie Connect in PHP # -This package provides Mollie OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). Visit our [API documentation](https://www.mollie.com/en/docs/oauth/overview) for more information about the Mollie implementation of OAuth2. +This package provides Mollie OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). Visit our [API documentation](https://docs.mollie.com/connect/getting-started) for more information about the Mollie implementation of OAuth2. -Use Mollie Connect (OAuth) to easily connect Mollie Merchant accounts to your application. [Mollie Connect](https://www.mollie.com/en/connect) also makes it possible to charge additional fees to your costumers with [Application Fee](https://www.mollie.com/en/docs/reference/payments/create#pfp-params). +Use Mollie Connect (OAuth) to easily connect Mollie Merchant accounts to your application. [Mollie Connect](https://docs.mollie.com/connect/overview) also makes it possible to charge additional fees to your costumers with [Application Fee](https://docs.mollie.com/connect/application-fees). ## Installation ## @@ -43,13 +43,13 @@ if (!isset($_GET['code'])) // (e.g. state). $authorizationUrl = $provider->getAuthorizationUrl([ // Optional, only use this if you want to ask for scopes the user previously denied. - 'approval_prompt' => 'force', - + 'approval_prompt' => 'force', + // Optional, a list of scopes. Defaults to only 'organizations.read'. 'scope' => [ - \Mollie\OAuth2\Client\Provider\Mollie::SCOPE_ORGANIZATIONS_READ, + \Mollie\OAuth2\Client\Provider\Mollie::SCOPE_ORGANIZATIONS_READ, \Mollie\OAuth2\Client\Provider\Mollie::SCOPE_PAYMENTS_READ, - ], + ], ]); // Get the state generated for you and store it to the session. @@ -75,10 +75,10 @@ else $accessToken = $provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'] ]); - + // Using the access token, we may look up details about the resource owner. $resourceOwner = $provider->getResourceOwner($accessToken); - + print_r($resourceOwner->toArray()); } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) @@ -115,6 +115,9 @@ $mollie->setAccessToken($token->getToken()); $payments = $mollie->payments->page(); ``` +> [!NOTE] +> In order to access the mollie api via `\Molie\Api\MollieApiClient`, the [mollie/mollie-api-php](github.com/mollie/mollie-api-php) library is required! + ### Revoking a token Both AccessTokens and RefreshTokens are revokable. Here's how to revoke an AccessToken: @@ -137,7 +140,7 @@ Similarly, here's how to revoke a RefreshToken: $provider = new \Mollie\OAuth2\Client\Provider\Mollie([ 'clientId' => 'YOUR_CLIENT_ID', 'clientSecret' => 'YOUR_CLIENT_SECRET', - 'redirectUri' => 'https://your-redirect-uri', + 'redirectUri' => 'https://your-redirect-uri',**** ]); $provider->revokeRefreshToken($refreshToken->getToken()); diff --git a/composer.json b/composer.json index 353a857..1ce7391 100644 --- a/composer.json +++ b/composer.json @@ -1,37 +1,60 @@ { - "name": "mollie/oauth2-mollie-php", - "description": "Mollie Provider for OAuth 2.0 Client", - "homepage": "https://github.com/mollie/oauth2-mollie-php", - "license": "BSD-2-Clause", - "authors": [ - { - "name": "Mollie B.V.", - "email": "info@mollie.com" - } - ], - "keywords": [ - "oauth", "oauth2", "client", "authorization", "authorisation", - "mollie", "payment", "service", "ideal", "creditcard", "mistercash", "bancontact", "sofort", "sofortbanking", - "sepa", "paypal", "paysafecard", "banktransfer", "direct debit", "belfius", "belfius direct net", - "refunds", "api", "payments", "gateway" - ], - "require": { - "php": ">=7.4.0", - "league/oauth2-client": "^1.0 || ^2.0", - "mollie/mollie-api-php": "^1.19 || ^2.39" - }, - "require-dev": { - "phpunit/phpunit": "^8.0|^9.0", - "mockery/mockery": "^0.9|^1.0" - }, - "autoload": { - "psr-4": { - "Mollie\\OAuth2\\Client\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Mollie\\OAuth2\\Client\\Test\\": "tests/src/" - } + "name": "mollie/oauth2-mollie-php", + "description": "Mollie Provider for OAuth 2.0 Client", + "homepage": "https://github.com/mollie/oauth2-mollie-php", + "license": "BSD-2-Clause", + "authors": [ + { + "name": "Mollie B.V.", + "email": "info@mollie.com" } + ], + "keywords": [ + "oauth", + "oauth2", + "client", + "authorization", + "authorisation", + "mollie", + "payment", + "service", + "ideal", + "creditcard", + "mistercash", + "bancontact", + "sofort", + "sofortbanking", + "sepa", + "paypal", + "paysafecard", + "banktransfer", + "direct debit", + "belfius", + "belfius direct net", + "refunds", + "api", + "payments", + "gateway" + ], + "require": { + "php": "^7.4|^8.0", + "league/oauth2-client": "^2.7" + }, + "require-dev": { + "phpunit/phpunit": "^9.6|^10.0", + "mockery/mockery": "^1.5" + }, + "suggest": { + "mollie/mollie-api-php": "To use the Mollie API client to interact with the Mollie API." + }, + "autoload": { + "psr-4": { + "Mollie\\OAuth2\\Client\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mollie\\OAuth2\\Client\\Test\\": "tests/src/" + } + } } diff --git a/phpstan.neon b/phpstan.neon index f71d635..c43f1fe 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,3 @@ parameters: - %currentWorkingDirectory%/tests excludes_analyse: - %currentWorkingDirectory%/vendor -includes: - - .phpstan.ignoreErrors.neon \ No newline at end of file diff --git a/src/Provider/Mollie.php b/src/Provider/Mollie.php index cd9d36f..6b743bd 100644 --- a/src/Provider/Mollie.php +++ b/src/Provider/Mollie.php @@ -4,17 +4,18 @@ use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; -use League\OAuth2\Client\Provider\ResourceOwnerInterface; use League\OAuth2\Client\Token\AccessToken; -use League\OAuth2\Client\Token\AccessTokenInterface; +use League\OAuth2\Client\Tool\BearerAuthorizationTrait; use Psr\Http\Message\ResponseInterface; class Mollie extends AbstractProvider { + use BearerAuthorizationTrait; + /** * Version of this client. */ - const CLIENT_VERSION = "2.7.0"; + const CLIENT_VERSION = "2.8.0"; /** * The base url to the Mollie API. @@ -113,7 +114,7 @@ public function __construct(array $options = [], array $collaborators = []) * @param string $url * @return Mollie */ - public function setMollieApiUrl($url) + public function setMollieApiUrl($url): self { $this->mollieApiUrl = $url; @@ -126,7 +127,7 @@ public function setMollieApiUrl($url) * @param string $url * @return Mollie */ - public function setMollieWebUrl($url) + public function setMollieWebUrl($url): self { $this->mollieWebUrl = $url; @@ -140,7 +141,7 @@ public function setMollieWebUrl($url) * * @return string */ - public function getBaseAuthorizationUrl() + public function getBaseAuthorizationUrl(): string { return $this->mollieWebUrl . '/oauth2/authorize'; } @@ -153,7 +154,7 @@ public function getBaseAuthorizationUrl() * @param array $params * @return string */ - public function getBaseAccessTokenUrl(array $params) + public function getBaseAccessTokenUrl(array $params): string { return $this->mollieApiUrl . '/oauth2/tokens'; } @@ -164,7 +165,7 @@ public function getBaseAccessTokenUrl(array $params) * @param AccessToken $token * @return string */ - public function getResourceOwnerDetailsUrl(AccessToken $token) + public function getResourceOwnerDetailsUrl(AccessToken $token): string { return static::MOLLIE_API_URL . '/v2/organizations/me'; } @@ -177,7 +178,7 @@ public function getResourceOwnerDetailsUrl(AccessToken $token) * @return \Psr\Http\Message\ResponseInterface * @throws \GuzzleHttp\Exception\GuzzleException */ - public function revokeAccessToken($accessToken) + public function revokeAccessToken($accessToken): ResponseInterface { return $this->revokeToken(self::TOKEN_TYPE_ACCESS, $accessToken); } @@ -190,7 +191,7 @@ public function revokeAccessToken($accessToken) * @return \Psr\Http\Message\ResponseInterface * @throws \GuzzleHttp\Exception\GuzzleException */ - public function revokeRefreshToken($refreshToken) + public function revokeRefreshToken($refreshToken): ResponseInterface { return $this->revokeToken(self::TOKEN_TYPE_REFRESH, $refreshToken); } @@ -204,7 +205,7 @@ public function revokeRefreshToken($refreshToken) * @return \Psr\Http\Message\ResponseInterface * @throws \GuzzleHttp\Exception\GuzzleException */ - public function revokeToken($type, $token) + public function revokeToken($type, $token): ResponseInterface { return $this->getRevokeTokenResponse([ 'token_type_hint' => $type, @@ -220,7 +221,7 @@ public function revokeToken($type, $token) * @return \Psr\Http\Message\ResponseInterface * @throws \GuzzleHttp\Exception\GuzzleException */ - protected function getRevokeTokenResponse(array $params) + protected function getRevokeTokenResponse(array $params): ResponseInterface { $params['client_id'] = $this->clientId; $params['client_secret'] = $this->clientSecret; @@ -244,7 +245,7 @@ protected function getRevokeTokenResponse(array $params) * * @return string[] */ - protected function getDefaultScopes() + protected function getDefaultScopes(): array { return [ self::SCOPE_ORGANIZATIONS_READ, @@ -257,7 +258,7 @@ protected function getDefaultScopes() * * @return string Scope separator, defaults to ',' */ - protected function getScopeSeparator() + protected function getScopeSeparator(): string { return ' '; } @@ -270,25 +271,27 @@ protected function getScopeSeparator() * @param array|string $data Parsed response data * @return void */ - protected function checkResponse(ResponseInterface $response, $data) + protected function checkResponse(ResponseInterface $response, $data): void { - if ($response->getStatusCode() >= 400) { - if (isset($data['error'])) { - if (isset($data['error']['type']) && isset($data['error']['message'])) { - $message = sprintf('[%s] %s', $data['error']['type'], $data['error']['message']); - } else { - $message = $data['error']; - } - - if (isset($data['error']['field'])) { - $message .= sprintf(' (field: %s)', $data['error']['field']); - } - } else { - $message = $response->getReasonPhrase(); - } - - throw new IdentityProviderException($message, $response->getStatusCode(), $response); + if ($response->getStatusCode() < 400) { + return; + } + + if (!isset($data['error'])) { + throw new IdentityProviderException($response->getReasonPhrase(), $response->getStatusCode(), $response); } + + if (isset($data['error']['type']) && isset($data['error']['message'])) { + $message = sprintf('[%s] %s', $data['error']['type'], $data['error']['message']); + } else { + $message = $data['error']; + } + + if (isset($data['error']['field'])) { + $message .= sprintf(' (field: %s)', $data['error']['field']); + } + + throw new IdentityProviderException($message, $response->getStatusCode(), $response); } /** @@ -297,29 +300,27 @@ protected function checkResponse(ResponseInterface $response, $data) * * @param array $response * @param AccessToken $token - * @return ResourceOwnerInterface + * @return MollieResourceOwner */ - protected function createResourceOwner(array $response, AccessToken $token) + protected function createResourceOwner(array $response, AccessToken $token): MollieResourceOwner { return new MollieResourceOwner($response); } /** - * Returns required authorization headers plus Mollie user agent strings. + * Returns the default headers used by this provider. + * + * Typically this is used to set 'Accept' or 'Content-Type' headers. * - * @param AccessTokenInterface|string|null $token Either a string or an access token instance * @return array */ - protected function getAuthorizationHeaders($token = null) + protected function getDefaultHeaders() { - $userAgent = implode(' ', [ - "MollieOAuth2PHP/" . self::CLIENT_VERSION, - "PHP/" . phpversion(), - ]); - return [ - 'Authorization' => 'Bearer ' . $token, - 'User-Agent' => $userAgent, + 'User-Agent' => implode(' ', [ + "MollieOAuth2PHP/" . self::CLIENT_VERSION, + "PHP/" . phpversion(), + ]) ]; } } diff --git a/src/Provider/MollieResourceOwner.php b/src/Provider/MollieResourceOwner.php index 1d67f7c..94b9c1a 100644 --- a/src/Provider/MollieResourceOwner.php +++ b/src/Provider/MollieResourceOwner.php @@ -1,52 +1,54 @@ -response = $response; - } + /** + * Set response + * + * @param array $response + */ + public function __construct(array $response) + { + $this->response = $response; + } - /** - * Returns the identifier of the authorized resource owner. - * - * @return string - */ - public function getId () - { - return $this->response['id']; - } + /** + * Returns the identifier of the authorized resource owner. + * + * @return string + */ + public function getId(): string + { + return $this->response['id']; + } - /** - * Return all of the owner details available as an array. - * - * @return array - */ - public function toArray () - { - return $this->response; - } + /** + * Return all of the owner details available as an array. + * + * @return array + */ + public function toArray(): array + { + return $this->response; + } /** * @return string|null */ - public function getEmail() + public function getEmail(): ?string { - return isset($this->response['email']) ? $this->response['email'] : null; + return $this->response['email'] ?? null; } /** @@ -54,7 +56,7 @@ public function getEmail() */ public function getRegistrationNumber() { - return isset($this->response['registrationNumber']) ? $this->response['registrationNumber'] : null; + return $this->response['registrationNumber'] ?? null; } /** @@ -62,6 +64,6 @@ public function getRegistrationNumber() */ public function getVatNumber() { - return isset($this->response['vatNumber']) ? $this->response['vatNumber'] : null; + return $this->response['vatNumber'] ?? null; } } diff --git a/tests/src/Provider/MollieTest.php b/tests/src/Provider/MollieTest.php index c762558..0f24257 100644 --- a/tests/src/Provider/MollieTest.php +++ b/tests/src/Provider/MollieTest.php @@ -1,6 +1,9 @@ -shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token", "token_type":"bearer"}'); + $response->shouldReceive('getBody')->andReturn(Utils::streamFor('{"access_token":"mock_access_token", "token_type":"bearer"}')); $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $response->shouldReceive('getStatusCode')->andReturn(200); @@ -106,7 +109,7 @@ public function testGetAccessToken() public function testRevokeToken() { $response = m::mock(ResponseInterface::class); - $response->shouldReceive('getBody')->andReturn('{"client_id":'.self::MOCK_CLIENT_ID.', "client_secret":'.self::MOCK_SECRET.', "redirect_uri":'.self::REDIRECT_URI.', "token_type_hint":"access_token":"mock_access_token"}'); + $response->shouldReceive('getBody')->andReturn('{"client_id":' . self::MOCK_CLIENT_ID . ', "client_secret":' . self::MOCK_SECRET . ', "redirect_uri":' . self::REDIRECT_URI . ', "token_type_hint":"access_token":"mock_access_token"}'); $response->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']); $response->shouldReceive('getStatusCode')->andReturn(204); @@ -123,7 +126,7 @@ public function testRevokeToken() public function testRevokeAccessToken() { $response = m::mock(ResponseInterface::class); - $response->shouldReceive('getBody')->andReturn('{"client_id":'.self::MOCK_CLIENT_ID.', "client_secret":'.self::MOCK_SECRET.', "redirect_uri":'.self::REDIRECT_URI.', "token_type_hint":"access_token":"mock_access_token"}'); + $response->shouldReceive('getBody')->andReturn('{"client_id":' . self::MOCK_CLIENT_ID . ', "client_secret":' . self::MOCK_SECRET . ', "redirect_uri":' . self::REDIRECT_URI . ', "token_type_hint":"access_token":"mock_access_token"}'); $response->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']); $response->shouldReceive('getStatusCode')->andReturn(204); @@ -140,7 +143,7 @@ public function testRevokeAccessToken() public function testRevokeRefreshToken() { $response = m::mock(ResponseInterface::class); - $response->shouldReceive('getBody')->andReturn('{"client_id":'.self::MOCK_CLIENT_ID.', "client_secret":'.self::MOCK_SECRET.', "redirect_uri":'.self::REDIRECT_URI.', "token_type_hint":"refresh_token":"mock_refresh_token"}'); + $response->shouldReceive('getBody')->andReturn('{"client_id":' . self::MOCK_CLIENT_ID . ', "client_secret":' . self::MOCK_SECRET . ', "redirect_uri":' . self::REDIRECT_URI . ', "token_type_hint":"refresh_token":"mock_refresh_token"}'); $response->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']); $response->shouldReceive('getStatusCode')->andReturn(204); @@ -160,7 +163,7 @@ public function testExceptionThrownWhenErrorObjectReceived() $status = rand(400, 600); $postResponse = m::mock(ResponseInterface::class); - $postResponse->shouldReceive('getBody')->andReturn('{"error":{"type":"request","message":"'.$message.'"}}'); + $postResponse->shouldReceive('getBody')->andReturn(Utils::streamFor('{"error":{"type":"request","message":"' . $message . '"}}')); $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $postResponse->shouldReceive('getStatusCode')->andReturn($status); @@ -178,14 +181,14 @@ public function testExceptionThrownWhenErrorObjectReceived() public function testUserData() { $postResponse = m::mock(ResponseInterface::class); - $postResponse->shouldReceive('getBody')->andReturn( + $postResponse->shouldReceive('getBody')->andReturn(Utils::streamFor( 'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token' - ); + )); $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'application/x-www-form-urlencoded']); $postResponse->shouldReceive('getStatusCode')->andReturn(200); $accountResponse = m::mock(ResponseInterface::class); - $accountResponse->shouldReceive('getBody')->andReturn( + $accountResponse->shouldReceive('getBody')->andReturn(Utils::streamFor( '{ "resource": "organization", "id": "org_12345678", @@ -242,7 +245,7 @@ public function testUserData() } } }' - ); + )); $accountResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); $accountResponse->shouldReceive('getStatusCode')->andReturn(200);