Skip to content

Commit

Permalink
drop guzzle and dependency, use psr http client and factories, drop p…
Browse files Browse the repository at this point in the history
…hp 8.2 support, use phpunit 11, phpstan 2, lcobucci/jwt 4 and 5
  • Loading branch information
frederikbosch committed Feb 11, 2025
1 parent 5424200 commit b58263c
Show file tree
Hide file tree
Showing 21 changed files with 258 additions and 491 deletions.
2 changes: 0 additions & 2 deletions .phpstan.test.neon

This file was deleted.

19 changes: 10 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand All @@ -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"
]
}
}
18 changes: 9 additions & 9 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
verbose="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true">
<coverage>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<testsuite name="GenkgoMail tests">
displayDetailsOnIncompleteTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnPhpunitDeprecations="true">
<testsuite name="GenkgoPush tests">
<directory>./test/Unit</directory>
<directory>./test/Integration</directory>
</testsuite>
Expand Down
56 changes: 16 additions & 40 deletions src/Apn/JwtAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, Token>
*/
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<int, Token> */
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();
}

Expand All @@ -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)
);

Expand Down
16 changes: 2 additions & 14 deletions src/Body.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/Exception/UnknownErrorException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);

namespace Genkgo\Push\Exception;

final class UnknownErrorException extends AbstractException
{
}
166 changes: 89 additions & 77 deletions src/Firebase/CloudMessaging.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,124 @@
namespace Genkgo\Push\Firebase;

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 GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;

final class CloudMessaging
final readonly class CloudMessaging
{
private const FCM_ENDPOINT = 'https://fcm.googleapis.com/v1';
private const string FCM_ENDPOINT = 'https://fcm.googleapis.com/v1';

/**
* @var AuthorizationHeaderProviderInterface
*/
private $authorizationHeaderProvider;

/**
* @var ClientInterface
*/
private $client;

/**
* @param ClientInterface $client
* @param AuthorizationHeaderProviderInterface $authorizationHeaderProvider
*/
public function __construct(
ClientInterface $client,
AuthorizationHeaderProviderInterface $authorizationHeaderProvider
private ClientInterface $client,
private RequestFactoryInterface&StreamFactoryInterface $requestFactory,
private AuthorizationHeaderProviderInterface $authorizationHeaderProvider
) {
$this->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<string|int, mixed> $data
* @param array<int|string, string|int|float|bool|array<int|string, string|int|float|bool>> $data
* @return array<string|int, string>
*/
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<string|int, string> $result */
Expand Down
Loading

0 comments on commit b58263c

Please sign in to comment.