diff --git a/.phpstan.test.neon b/.phpstan.test.neon deleted file mode 100644 index efea1bb..0000000 --- a/.phpstan.test.neon +++ /dev/null @@ -1,2 +0,0 @@ -includes: -- vendor/phpstan/phpstan-phpunit/extension.neon \ No newline at end of file diff --git a/composer.json b/composer.json index e3f9416..6c33da1 100644 --- a/composer.json +++ b/composer.json @@ -2,17 +2,18 @@ "name" : "genkgo/push", "description": "Send push messages to Android and Apple using one interface.", "require" : { - "php": "~8.2.0 || ~8.3.0", + "php": "~8.3.0", "ext-json" : "*", - "apple/apn-push": "^3.0", - "guzzlehttp/guzzle": "^7.0", - "lcobucci/jwt": "^4.1.4" + "apple/apn-push": "^v3.1.7", + "lcobucci/jwt": "^4.1.4 || ^5.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" }, "require-dev" : { - "phpunit/phpunit": "^9", - "phpstan/phpstan": "^1", - "phpstan/phpstan-phpunit": "^1", - "friendsofphp/php-cs-fixer": "^3.0" + "guzzlehttp/guzzle": "^7.9.2", + "phpunit/phpunit": "^11.5.7", + "phpstan/phpstan": "^2.1.4", + "friendsofphp/php-cs-fixer": "^v3.68.5" }, "autoload" : { "psr-4" : { @@ -32,7 +33,7 @@ "./vendor/bin/phpunit -c phpunit.xml", "./vendor/bin/php-cs-fixer fix --verbose --dry-run --config .php-cs-fixer.dist.php ./src ./test", "./vendor/bin/phpstan analyse -l max src", - "./vendor/bin/phpstan analyse -l max -c .phpstan.test.neon test" + "./vendor/bin/phpstan analyse -l max test" ] } } diff --git a/phpunit.xml b/phpunit.xml index 000d85f..7499f53 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,16 +1,16 @@ - - - ./src - - - + displayDetailsOnIncompleteTests="true" + displayDetailsOnSkippedTests="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnPhpunitDeprecations="true"> + ./test/Unit ./test/Integration diff --git a/src/Apn/JwtAuthenticator.php b/src/Apn/JwtAuthenticator.php index 8673588..f37bdf0 100644 --- a/src/Apn/JwtAuthenticator.php +++ b/src/Apn/JwtAuthenticator.php @@ -5,52 +5,27 @@ use Apple\ApnPush\Protocol\Http\Authenticator\AuthenticatorInterface; use Apple\ApnPush\Protocol\Http\Request; -use Lcobucci\JWT\Builder; use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter; use Lcobucci\JWT\Signer\Ecdsa\Sha256; -use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Token; final class JwtAuthenticator implements AuthenticatorInterface { - /** - * @var string - */ - private $token; - - /** - * @var string - */ - private $keyId; - - /** - * @var string - */ - private $teamId; - - /** - * @var string - */ - private $refreshAfter; - - /** - * @var \Iterator - */ - private $tokenGenerator; - - /** - * @param string $token - * @param string $keyId - * @param string $teamId - * @param string $refreshAfter - */ - public function __construct(string $token, string $keyId, string $teamId, string $refreshAfter = 'PT30M') - { - $this->token = $token; - $this->keyId = $keyId; - $this->teamId = $teamId; - $this->refreshAfter = $refreshAfter; + /** @var \Iterator */ + private \Iterator $tokenGenerator; + + public function __construct( + /** @var non-empty-string */ + private readonly string $token, + /** @var non-empty-string */ + private readonly string $keyId, + /** @var non-empty-string */ + private readonly string $teamId, + /** @var non-empty-string */ + private readonly string $refreshAfter = 'PT30M' + ) { $this->tokenGenerator = $this->newGenerator(); } @@ -71,8 +46,9 @@ private function newGenerator(): \Iterator throw new \UnexpectedValueException('Cannot fetch token content from ' . $this->token . ', empty file'); } + // support both lcobucci/jwt 4 and 5 $configuration = Configuration::forSymmetricSigner( - Sha256::create(), + new Sha256(new MultibyteStringConverter()), InMemory::plainText($keyContent) ); diff --git a/src/Body.php b/src/Body.php index 066660d..c6a9808 100644 --- a/src/Body.php +++ b/src/Body.php @@ -3,24 +3,12 @@ namespace Genkgo\Push; -final class Body +final readonly class Body { - /** - * @var string - */ - private $body; - - /** - * @param string $body - */ - public function __construct(string $body) + public function __construct(private string $body) { - $this->body = $body; } - /** - * @return string - */ public function __toString(): string { return $this->body; diff --git a/src/Exception/UnknownErrorException.php b/src/Exception/UnknownErrorException.php new file mode 100644 index 0000000..e78fe56 --- /dev/null +++ b/src/Exception/UnknownErrorException.php @@ -0,0 +1,8 @@ +authorizationHeaderProvider = $authorizationHeaderProvider; - $this->client = $client; } /** - * @param string $projectId - * @param string $token - * @param Notification $notification + * @throws UnknownRecipientException + * @throws ForbiddenToSendMessageException + * @throws ClientExceptionInterface + * @throws UnknownErrorException + * @throws InvalidRecipientException + * @throws InvalidMessageException */ public function send(string $projectId, string $token, Notification $notification): void { $authorizationHeader = \call_user_func($this->authorizationHeaderProvider); - try { - $json = \json_encode([ - 'message' => [ - 'token' => $token, - 'data' => $this->convertDataToStrings($notification->getData()), - 'notification' => [ - 'body' => $notification->getBody(), - 'title' => $notification->getTitle(), - ] + $json = \json_encode([ + 'message' => [ + 'token' => $token, + 'data' => $this->convertDataToStrings($notification->getData()), + 'notification' => [ + 'body' => $notification->getBody(), + 'title' => $notification->getTitle(), ] - ]); + ] + ]); - if ($json === false) { - throw new \UnexpectedValueException('Cannot encode HTTP message'); - } + if ($json === false) { + throw new \UnexpectedValueException('Cannot encode HTTP message'); + } - $this->client - ->send( - new Request( - 'POST', - \sprintf( - '%s/projects/%s/messages:send', - self::FCM_ENDPOINT, - $projectId - ), - [ - 'Content-Type' => 'application/json', - 'Authorization' => $authorizationHeader, - ], - $json - ) - ); - } catch (ClientException $e) { - $response = $e->getResponse(); - if ($response !== null && $response->getStatusCode() === 403) { - throw new ForbiddenToSendMessageException( - 'Cannot send message due to access restriction:' . $e->getMessage(), - $e->getCode(), - $e - ); - } + $response = $this->client + ->sendRequest( + $this->requestFactory->createRequest( + 'POST', + \sprintf( + '%s/projects/%s/messages:send', + self::FCM_ENDPOINT, + $projectId + ), + ) + ->withHeader('Content-Type', 'application/json') + ->withHeader('Authorization', $authorizationHeader) + ->withBody($this->requestFactory->createStream($json)) + ); + + if ($response->getStatusCode() < 300) { + return; + } - if ($response !== null && $response->getStatusCode() === 404) { - throw new UnknownRecipientException( - 'Cannot send message, unknown recipient:' . $e->getMessage(), - $e->getCode(), - $e - ); + $contentTypeHeader = $response->getHeaderLine('content-type'); + if (!\str_contains($contentTypeHeader, 'application/json')) { + throw new UnknownErrorException('Cannot send message, unknown error. Got status code: ' . $response->getStatusCode()); + } else { + $responseText = (string)$response->getBody(); + try { + /** @var array{error: array{message: string}}|null $responseJson */ + $responseJson = \json_decode($responseText, true, 512, \JSON_THROW_ON_ERROR); + $error = $responseJson['error']['message'] ?? ''; + } catch (\JsonException) { + $error = ''; } + } + + if ($response->getStatusCode() === 400) { + throw new InvalidRecipientException($error); + } + + if ($response->getStatusCode() === 404) { + throw new UnknownRecipientException($error); + } + + if ($response->getStatusCode() === 403) { + throw new ForbiddenToSendMessageException($error); + } - throw $e; + if ($response->getStatusCode() === 429) { + throw new ForbiddenToSendMessageException($error); } + + if ($response->getStatusCode() === 401) { + throw new InvalidMessageException($error); + } + + throw new UnknownErrorException('Cannot send message, unknown error. Got status code: ' . $response->getStatusCode()); } /** - * @param array $data + * @param array> $data * @return array */ private function convertDataToStrings(array $data): array { - $callback = function ($item) { - return (string)$item; - }; + $callback = fn (string|int|float|bool|null $item): string => (string)$item; + + $func = function (array|string|int|float|bool|null $item) use (&$func, &$callback) { + if (\is_array($item)) { + /** @var callable(mixed): mixed $func */ + return \array_map($func, $item); + } - $func = function ($item) use (&$func, &$callback) { - return \is_array($item) ? \array_map($func, $item) : \call_user_func($callback, $item); + return $callback($item); }; /** @var array $result */ diff --git a/src/Firebase/Notification.php b/src/Firebase/Notification.php index fdc6b0c..a327592 100644 --- a/src/Firebase/Notification.php +++ b/src/Firebase/Notification.php @@ -3,53 +3,30 @@ namespace Genkgo\Push\Firebase; -final class Notification +final readonly class Notification { /** - * @var string + * @param array> $data */ - private $body; - - /** - * @var string - */ - private $title; - - /** - * @var array - */ - private $data; - - /** - * @param string $body - * @param string $title - * @param array $data - */ - public function __construct(string $body, string $title, array $data = []) - { - $this->body = $body; - $this->title = $title; - $this->data = $data; + public function __construct( + private string $body, + private string $title, + private array $data = [] + ) { } - /** - * @return string - */ public function getBody(): string { return $this->body; } - /** - * @return string - */ public function getTitle(): string { return $this->title; } /** - * @return array + * @return array> */ public function getData(): array { diff --git a/src/Firebase/OauthBearerTokenProvider.php b/src/Firebase/OauthBearerTokenProvider.php index e83e993..8871f52 100644 --- a/src/Firebase/OauthBearerTokenProvider.php +++ b/src/Firebase/OauthBearerTokenProvider.php @@ -3,41 +3,24 @@ namespace Genkgo\Push\Firebase; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Psr7\Request; -use Lcobucci\JWT\Builder; use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Signer\Key; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; -final class OauthBearerTokenProvider implements AuthorizationHeaderProviderInterface +final readonly class OauthBearerTokenProvider implements AuthorizationHeaderProviderInterface { - private const AUTH_ENDPOINT = 'https://www.googleapis.com/oauth2/v4/token'; + private const string AUTH_ENDPOINT = 'https://www.googleapis.com/oauth2/v4/token'; - /** - * @var ClientInterface - */ - private $client; - - /** - * @var string - */ - private $serviceAccountFile; - - /** - * @param ClientInterface $client - * @param string $serviceAccountFile - */ - public function __construct(ClientInterface $client, string $serviceAccountFile) - { - $this->client = $client; - $this->serviceAccountFile = $serviceAccountFile; + public function __construct( + private ClientInterface $client, + private RequestFactoryInterface&StreamFactoryInterface $requestFactory, + private string $serviceAccountFile + ) { } - /** - * @return string - */ public function __invoke(): string { $serviceAccount = \file_get_contents($this->serviceAccountFile); @@ -45,15 +28,12 @@ public function __invoke(): string throw new \UnexpectedValueException('Cannot read service account ' . $this->serviceAccountFile); } + /** @var array{private_key: non-empty-string, client_email: non-empty-string}|false $googleJson */ $googleJson = \json_decode($serviceAccount, true); - if (!$googleJson || !\is_array($googleJson)) { + if (!$googleJson) { throw new \UnexpectedValueException('Invalid service account file ' . $this->serviceAccountFile . ' passed, cannot decode json.'); } - if (!\array_key_exists('private_key', $googleJson)) { - throw new \UnexpectedValueException('Expecting key `private_key` in service account.'); - } - $configuration = Configuration::forSymmetricSigner( new Sha256(), InMemory::plainText($googleJson['private_key']) @@ -70,27 +50,30 @@ public function __invoke(): string ->permittedFor(self::AUTH_ENDPOINT); $authResponse = (string)$this->client - ->send( - new Request( + ->sendRequest( + $this->requestFactory->createRequest( 'POST', self::AUTH_ENDPOINT, - [ - 'Content-Type' => 'application/x-www-form-urlencoded', - ], - \http_build_query([ - 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', - 'assertion' => $builder->getToken( - $configuration->signer(), - $configuration->signingKey() - )->toString() - ]) ) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody( + $this->requestFactory->createStream( + \http_build_query([ + 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $builder->getToken( + $configuration->signer(), + $configuration->signingKey() + )->toString() + ]) + ) + ) ) ->getBody(); + /** @var array{access_token: non-empty-string}|false $authTokens */ $authTokens = \json_decode($authResponse, true); - if (!$authTokens || !\is_array($authTokens) || !\array_key_exists('private_key', $authTokens)) { + if (!$authTokens) { throw new \UnexpectedValueException('Expecting key `access_token` from Oauth request.'); } diff --git a/src/Gateway.php b/src/Gateway.php index 2607c81..c613c5a 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -5,24 +5,16 @@ use Genkgo\Push\Exception\UnsupportedMessageRecipient; -final class Gateway +final readonly class Gateway { /** - * @var array|SenderInterface[] + * @param array $senders */ - private $senders = []; - - /** - * @param array|SenderInterface[] $senders - */ - public function __construct(array $senders) + public function __construct(private array $senders) { - $this->senders = $senders; } /** - * @param Message $message - * @param RecipientInterface $recipient * @throws Exception\ForbiddenToSendMessageException * @throws Exception\InvalidMessageException * @throws Exception\InvalidRecipientException @@ -38,7 +30,7 @@ public function send(Message $message, RecipientInterface $recipient): void } } - $recipientClass = \get_class($recipient); + $recipientClass = $recipient::class; throw new UnsupportedMessageRecipient( "Could not find a sender that supports the combination of message and recipient ({$recipientClass})" ); diff --git a/src/Message.php b/src/Message.php index 82e63be..f714d01 100644 --- a/src/Message.php +++ b/src/Message.php @@ -5,48 +5,29 @@ final class Message { - /** - * @var Body - */ - private $body; + private Title $title; - /** - * @var Title - */ - private $title; - - /** - * @var array - */ - private $extra = []; + /** @var array> */ + private array $extra; - /** - * @param Body $body - */ - public function __construct(Body $body) + public function __construct(private readonly Body $body) { - $this->body = $body; $this->title = new Title(''); + $this->extra = []; } - /** - * @return Body - */ public function getBody(): Body { return $this->body; } - /** - * @return Title - */ public function getTitle(): Title { return $this->title; } /** - * @return array + * @return array> */ public function getExtra(): array { @@ -65,11 +46,9 @@ public function withTitle(Title $title): self } /** - * @param mixed $key - * @param mixed $value - * @return Message + * @param string|int|float|bool|array $value */ - public function withExtra($key, $value): self + public function withExtra(string|int $key, mixed $value): self { $clone = clone $this; $clone->extra[$key] = $value; diff --git a/src/Recipient/AndroidDeviceRecipient.php b/src/Recipient/AndroidDeviceRecipient.php index 45c7297..59ce636 100644 --- a/src/Recipient/AndroidDeviceRecipient.php +++ b/src/Recipient/AndroidDeviceRecipient.php @@ -5,34 +5,18 @@ use Genkgo\Push\RecipientInterface; -final class AndroidDeviceRecipient implements RecipientInterface +final readonly class AndroidDeviceRecipient implements RecipientInterface { - /** - * @var string - */ - private $token; - - /** - * @param string $token - */ - public function __construct(string $token) + public function __construct(private string $token) { - $this->token = $token; } - /** - * @return string - */ public function getToken(): string { return $this->token; } - /** - * @param string $token - * @return RecipientInterface - */ - public static function fromString(string $token): RecipientInterface + public static function fromString(string $token): self { return new self($token); } diff --git a/src/Recipient/AppleDeviceRecipient.php b/src/Recipient/AppleDeviceRecipient.php index 34633f8..eb103ad 100644 --- a/src/Recipient/AppleDeviceRecipient.php +++ b/src/Recipient/AppleDeviceRecipient.php @@ -5,34 +5,18 @@ use Genkgo\Push\RecipientInterface; -final class AppleDeviceRecipient implements RecipientInterface +final readonly class AppleDeviceRecipient implements RecipientInterface { - /** - * @var string - */ - private $token; - - /** - * @param string $token - */ - public function __construct(string $token) + public function __construct(private string $token) { - $this->token = $token; } - /** - * @return string - */ public function getToken(): string { return $this->token; } - /** - * @param string $token - * @return RecipientInterface - */ - public static function fromString(string $token): RecipientInterface + public static function fromString(string $token): self { return new self($token); } diff --git a/src/Recipient/FirebaseRecipient.php b/src/Recipient/FirebaseRecipient.php index e66fb4d..29d3a85 100644 --- a/src/Recipient/FirebaseRecipient.php +++ b/src/Recipient/FirebaseRecipient.php @@ -5,34 +5,18 @@ use Genkgo\Push\RecipientInterface; -final class FirebaseRecipient implements RecipientInterface +final readonly class FirebaseRecipient implements RecipientInterface { - /** - * @var string - */ - private $token; - - /** - * @param string $token - */ - public function __construct(string $token) + public function __construct(private string $token) { - $this->token = $token; } - /** - * @return string - */ public function getToken(): string { return $this->token; } - /** - * @param string $token - * @return RecipientInterface - */ - public static function fromString(string $token): RecipientInterface + public static function fromString(string $token): self { return new self($token); } diff --git a/src/RecipientInterface.php b/src/RecipientInterface.php index fce0e94..e5ec2da 100644 --- a/src/RecipientInterface.php +++ b/src/RecipientInterface.php @@ -5,14 +5,7 @@ interface RecipientInterface { - /** - * @return string - */ public function getToken(): string; - /** - * @param string $token - * @return RecipientInterface - */ - public static function fromString(string $token): RecipientInterface; + public static function fromString(string $token): self; } diff --git a/src/Sender/AppleApnSender.php b/src/Sender/AppleApnSender.php index 5f0873f..005ee60 100644 --- a/src/Sender/AppleApnSender.php +++ b/src/Sender/AppleApnSender.php @@ -4,6 +4,7 @@ namespace Genkgo\Push\Sender; use Apple\ApnPush\Certificate\Certificate; +use Apple\ApnPush\Exception\CertificateFileNotFoundException; use Apple\ApnPush\Exception\SendNotification\BadCertificateEnvironmentException; use Apple\ApnPush\Exception\SendNotification\BadCertificateException; use Apple\ApnPush\Exception\SendNotification\BadDeviceTokenException; @@ -23,6 +24,7 @@ use Apple\ApnPush\Exception\SendNotification\MissingProviderTokenException; use Apple\ApnPush\Exception\SendNotification\MissingTopicException; use Apple\ApnPush\Exception\SendNotification\PayloadEmptyException; +use Apple\ApnPush\Exception\SendNotification\SendNotificationException; use Apple\ApnPush\Exception\SendNotification\TopicDisallowedException; use Apple\ApnPush\Exception\SendNotification\UndefinedErrorException; use Apple\ApnPush\Exception\SendNotification\UnregisteredException; @@ -46,48 +48,28 @@ use Genkgo\Push\RecipientInterface; use Genkgo\Push\SenderInterface; -final class AppleApnSender implements SenderInterface +final readonly class AppleApnSender implements SenderInterface { - /** - * @var AppleSender - */ - private $sender; - - /** - * @var string - */ - private $bundleId; - - /** - * @var bool - */ - private $sandbox; - - /** - * @param AppleSender $sender - * @param string $bundleId - * @param bool $sandbox - */ - public function __construct(AppleSender $sender, string $bundleId, bool $sandbox = false) - { - $this->sender = $sender; - $this->sandbox = $sandbox; - $this->bundleId = $bundleId; + public function __construct( + private AppleSender $sender, + private string $bundleId, + private bool $sandbox = false + ) { } - /** - * @param Message $message - * @param RecipientInterface $recipient - * @return bool - */ public function supports(Message $message, RecipientInterface $recipient): bool { return $recipient instanceof AppleDeviceRecipient; } /** - * @param Message $message - * @param RecipientInterface|AppleDeviceRecipient $recipient + * @param RecipientInterface&AppleDeviceRecipient $recipient + * @throws ConnectionException + * @throws ForbiddenToSendMessageException + * @throws InvalidMessageException + * @throws InvalidRecipientException + * @throws UnknownRecipientException + * @throws SendNotificationException * @codeCoverageIgnore */ public function send(Message $message, RecipientInterface $recipient): void @@ -159,10 +141,7 @@ public function send(Message $message, RecipientInterface $recipient): void } /** - * @param string $certificate - * @param string $passphrase - * @param bool $sandboxMode - * @return AppleApnSender + * @throws CertificateFileNotFoundException */ public static function fromCertificate(string $certificate, string $passphrase, bool $sandboxMode = false): self { @@ -176,12 +155,10 @@ public static function fromCertificate(string $certificate, string $passphrase, } /** - * @param string $token - * @param string $keyId - * @param string $teamId - * @param string $bundleId - * @param bool $sandboxMode - * @return AppleApnSender + * @param non-empty-string $token + * @param non-empty-string $keyId + * @param non-empty-string $teamId + * @param non-empty-string $bundleId */ public static function fromToken( string $token, diff --git a/src/Sender/FirebaseSender.php b/src/Sender/FirebaseSender.php index aded630..3b16501 100644 --- a/src/Sender/FirebaseSender.php +++ b/src/Sender/FirebaseSender.php @@ -6,6 +6,7 @@ use Genkgo\Push\Exception\ForbiddenToSendMessageException; use Genkgo\Push\Exception\InvalidMessageException; use Genkgo\Push\Exception\InvalidRecipientException; +use Genkgo\Push\Exception\UnknownErrorException; use Genkgo\Push\Exception\UnknownRecipientException; use Genkgo\Push\Firebase\CloudMessaging; use Genkgo\Push\Firebase\Notification; @@ -13,28 +14,14 @@ use Genkgo\Push\Recipient\FirebaseRecipient; use Genkgo\Push\RecipientInterface; use Genkgo\Push\SenderInterface; -use GuzzleHttp\Exception\RequestException; +use Psr\Http\Client\ClientExceptionInterface; -final class FirebaseSender implements SenderInterface +final readonly class FirebaseSender implements SenderInterface { - /** - * @var CloudMessaging - */ - private $cloudMessaging; - - /** - * @var string - */ - private $projectId; - - /** - * @param CloudMessaging $cloudMessaging - * @param string $projectId - */ - public function __construct(CloudMessaging $cloudMessaging, string $projectId) - { - $this->cloudMessaging = $cloudMessaging; - $this->projectId = $projectId; + public function __construct( + private CloudMessaging $cloudMessaging, + private string $projectId + ) { } /** @@ -48,64 +35,24 @@ public function supports(Message $message, RecipientInterface $recipient): bool } /** - * @param Message $message - * @param RecipientInterface $recipient - * @return void + * @throws ForbiddenToSendMessageException + * @throws InvalidMessageException + * @throws InvalidRecipientException + * @throws UnknownErrorException + * @throws UnknownRecipientException + * @throws ClientExceptionInterface * @codeCoverageIgnore */ public function send(Message $message, RecipientInterface $recipient): void { - try { - $this->cloudMessaging->send( - $this->projectId, - $recipient->getToken(), - new Notification( - (string)$message->getBody(), - (string)$message->getTitle(), - $message->getExtra() - ) - ); - } catch (RequestException $e) { - $response = $e->getResponse(); - if (!$response) { - throw $e; - } - - $contentTypeHeader = $response->getHeaderLine('content-type'); - if (\strpos($contentTypeHeader, 'application/json') === false) { - $error = $e->getMessage(); - } else { - $responseText = (string)$response->getBody(); - $responseJson = \json_decode($responseText, true); - - if ($responseJson && \is_array($responseJson) && isset($responseJson['error']['message'])) { - $error = $responseJson['error']['message']; - } else { - $error = $e->getMessage(); - } - } - - if ($response->getStatusCode() === 400) { - throw new InvalidRecipientException($error); - } - - if ($response->getStatusCode() === 404) { - throw new UnknownRecipientException($error); - } - - if ($response->getStatusCode() === 403) { - throw new ForbiddenToSendMessageException($error); - } - - if ($response->getStatusCode() === 429) { - throw new ForbiddenToSendMessageException($error); - } - - if ($response->getStatusCode() === 401) { - throw new InvalidMessageException($error); - } - - throw $e; - } + $this->cloudMessaging->send( + $this->projectId, + $recipient->getToken(), + new Notification( + (string)$message->getBody(), + (string)$message->getTitle(), + $message->getExtra() + ) + ); } } diff --git a/src/SenderInterface.php b/src/SenderInterface.php index 2502ef2..660246f 100644 --- a/src/SenderInterface.php +++ b/src/SenderInterface.php @@ -10,17 +10,9 @@ interface SenderInterface { - /** - * @param Message $message - * @param RecipientInterface $recipient - * @return bool - */ public function supports(Message $message, RecipientInterface $recipient): bool; /** - * @param Message $message - * @param RecipientInterface $recipient - * @return void * @throws ForbiddenToSendMessageException * @throws InvalidMessageException * @throws InvalidRecipientException diff --git a/src/Title.php b/src/Title.php index b8fa0de..be2818d 100644 --- a/src/Title.php +++ b/src/Title.php @@ -3,24 +3,12 @@ namespace Genkgo\Push; -final class Title +final readonly class Title { - /** - * @var string - */ - private $title; - - /** - * @param string $title - */ - public function __construct(string $title) + public function __construct(private string $title) { - $this->title = $title; } - /** - * @return string - */ public function __toString(): string { return $this->title; diff --git a/test/Integration/CloudMessagingTest.php b/test/Integration/CloudMessagingTest.php index 39677a6..f3c9ea9 100644 --- a/test/Integration/CloudMessagingTest.php +++ b/test/Integration/CloudMessagingTest.php @@ -8,10 +8,12 @@ use Genkgo\Push\Firebase\AuthorizationHeaderProviderInterface; use Genkgo\Push\Firebase\CloudMessaging; use Genkgo\Push\Firebase\Notification; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\HttpFactory; use GuzzleHttp\Psr7\Response; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamFactoryInterface; final class CloudMessagingTest extends AbstractTestCase { @@ -24,10 +26,10 @@ public function testDataIsString(): void $client = $this->createMock(ClientInterface::class); $client - ->method('send') + ->method('sendRequest') ->with( $this->callback( - function (Request $request) { + function (RequestInterface $request) { $body = \json_decode((string)$request->getBody(), true); $this->assertSame("1", $body['message']['data']['true']); // @phpstan-ignore-line $this->assertSame("", $body['message']['data']['false']); // @phpstan-ignore-line @@ -47,7 +49,7 @@ function (Request $request) { ] ); - $cloudMessaging = new CloudMessaging($client, $provider); + $cloudMessaging = new CloudMessaging($client, new HttpFactory(), $provider); $cloudMessaging->send('project-xyz', 'token', $notification); } @@ -60,21 +62,20 @@ public function testForbidden(): void ->method('__invoke') ->willReturn('Bearer test'); + $httpFactory = new HttpFactory(); $client = $this->createMock(ClientInterface::class); $client - ->method('send') + ->method('sendRequest') ->willReturnCallback( - function (Request $request) { - throw new ClientException( - 'Forbidden', - $request, - new Response(403) - ); - } + fn () => new Response( + 403, + ['Content-Type' => 'application/json'], + $httpFactory->createStream((string)\json_encode(['error' => ['message' => 'Forbidden']])) + ) ); $notification = new Notification('body', 'title'); - $cloudMessaging = new CloudMessaging($client, $provider); + $cloudMessaging = new CloudMessaging($client, $httpFactory, $provider); $cloudMessaging->send('project-xyz', 'token', $notification); } diff --git a/test/Unit/Sender/FirebaseSenderTest.php b/test/Unit/Sender/FirebaseSenderTest.php index 1bae083..e933393 100644 --- a/test/Unit/Sender/FirebaseSenderTest.php +++ b/test/Unit/Sender/FirebaseSenderTest.php @@ -11,7 +11,10 @@ use Genkgo\Push\Recipient\AndroidDeviceRecipient; use Genkgo\Push\Recipient\FirebaseRecipient; use Genkgo\Push\Sender\FirebaseSender; -use GuzzleHttp\ClientInterface; +use GuzzleHttp\Psr7\HttpFactory; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; final class FirebaseSenderTest extends AbstractTestCase { @@ -23,7 +26,7 @@ public function testSupports(): void $client = $this->createMock(ClientInterface::class); $authorization = $this->createMock(AuthorizationHeaderProviderInterface::class); - $sender = new FirebaseSender(new CloudMessaging($client, $authorization), '1234'); + $sender = new FirebaseSender(new CloudMessaging($client, new HttpFactory(), $authorization), '1234'); $this->assertTrue($sender->supports($message, $recipient)); } @@ -35,7 +38,7 @@ public function testNotSupports(): void $client = $this->createMock(ClientInterface::class); $authorization = $this->createMock(AuthorizationHeaderProviderInterface::class); - $sender = new FirebaseSender(new CloudMessaging($client, $authorization), '1234'); + $sender = new FirebaseSender(new CloudMessaging($client, new HttpFactory(), $authorization), '1234'); $this->assertFalse($sender->supports($message, $recipient)); } }