Verified Commit 56fd0a6b authored by Richard Weinhold's avatar Richard Weinhold 🔨

adds better error/exception handling

parent 44e2d8b9
......@@ -130,8 +130,20 @@ $result = $push->send($message, [...]);
$errors = $result->getFailed();
```
Error are saved as Exeptions, so it's possible to just throw them. To simply just throw the first error if one occurred, call:
Errors are handled as Exeptions, so it's possible to just throw them. To simply just throw the first error if one occurred, call:
```php
$push->send($message, [...])->throwOnFirstError();
$push->send($message, [...])->throwOnFirstException();
```
***Be aware***: Sometimes other failures than usage-errors occur. APNS (HTTP2) can respond with explicit reasons, which will be handled as `ResponseReasonException`. It's a good idea to not just throw them, but handle them other ways. E.g. you might want to delete or update device-tokens which were marked as invalid.
```php
use \ricwein\PushNotification\Exceptions\ResponseReasonException;
foreach($result->getFailed() as $token => $error) {
if ($error instanceof ResponseReasonException && $error->isInvalidDeviceToken()) {
// do something with this information.
}
}
```
<?php
namespace ricwein\PushNotification\Exceptions;
use Exception;
class PushException extends Exception
{
}
<?php
namespace ricwein\PushNotification\Exceptions;
class RequestException extends PushException
{
}
<?php
namespace ricwein\PushNotification\Exceptions;
class ResponseException extends PushException
{
}
<?php
namespace ricwein\PushNotification\Exceptions;
use Throwable;
class ResponseReasonException extends ResponseException
{
public const REASON_UNKNOWN = 'UNKNOWN';
public const REASON_BAD_DEVICE_TOKEN = 'BadDeviceToken';
public const REASON_BAD_COLLAPSE_ID = 'BadCollapseId';
public const REASON_BAD_EXPIRATION_DATE = 'BadExpirationDate';
public const REASON_BAD_MESSAGE_ID = 'BadMessageId';
public const REASON_BAD_PRIORITY = 'BadPriority';
public const REASON_BAD_TOPIC = 'BadTopic';
public const REASON_DEVICE_TOKEN_NOT_FOR_TOPIC = 'DeviceTokenNotForTopic';
public const REASON_DUPLICATE_HEADERS = 'DuplicateHeaders';
public const REASON_IDLE_TIMEOUT = 'IdleTimeout';
public const REASON_MISSING_DEVICE_TOKEN = 'MissingDeviceToken';
public const REASON_MISSING_TOPIC = 'MissingTopic';
public const REASON_PAYLOAD_EMPTY = 'PayloadEmpty';
public const REASON_TOPIC_DISALLOWED = 'TopicDisallowed';
public const REASON_BAD_CERTIFICATE = 'BadCertificate';
public const REASON_BAD_CERTIFICATE_ENVIRONMENT = 'BadCertificateEnvironment';
public const REASON_EXPIRED_PROVIDER_TOKEN = 'ExpiredProviderToken';
public const REASON_FORBIDDEN = 'Forbidden';
public const REASON_INVALID_PROVIDER_TOKEN = 'InvalidProviderToken';
public const REASON_MISSING_PROVIDER_TOKEN = 'MissingProviderToken';
public const REASON_BAD_PATH = 'BadPath';
public const REASON_METHOD_NOT_ALLOWED = 'MethodNotAllowed';
public const REASON_UNREGISTERED = 'Unregistered';
public const REASON_PAYLOAD_TOO_LARGE = 'PayloadTooLarge';
public const REASON_TOO_MANY_PROVIDER_TOKEN_UPDATES = 'TooManyProviderTokenUpdates';
public const REASON_TOO_MANY_REQUESTS = 'TooManyRequests';
public const REASON_INTERNAL_SERVER_ERROR = 'InternalServerError';
public const REASON_SERVICE_UNAVAILABLE = 'ServiceUnavailable';
public const REASON_SHUTDOWN = 'Shutdown';
public const GROUP_VALID_REASONS = [
self::REASON_BAD_DEVICE_TOKEN,
self::REASON_BAD_COLLAPSE_ID,
self::REASON_BAD_EXPIRATION_DATE,
self::REASON_BAD_MESSAGE_ID,
self::REASON_BAD_PRIORITY,
self::REASON_BAD_TOPIC,
self::REASON_DEVICE_TOKEN_NOT_FOR_TOPIC,
self::REASON_DUPLICATE_HEADERS,
self::REASON_IDLE_TIMEOUT,
self::REASON_MISSING_DEVICE_TOKEN,
self::REASON_MISSING_TOPIC,
self::REASON_PAYLOAD_EMPTY,
self::REASON_TOPIC_DISALLOWED,
self::REASON_BAD_CERTIFICATE,
self::REASON_BAD_CERTIFICATE_ENVIRONMENT,
self::REASON_EXPIRED_PROVIDER_TOKEN,
self::REASON_FORBIDDEN,
self::REASON_INVALID_PROVIDER_TOKEN,
self::REASON_MISSING_PROVIDER_TOKEN,
self::REASON_BAD_PATH,
self::REASON_METHOD_NOT_ALLOWED,
self::REASON_UNREGISTERED,
self::REASON_PAYLOAD_TOO_LARGE,
self::REASON_TOO_MANY_PROVIDER_TOKEN_UPDATES,
self::REASON_TOO_MANY_REQUESTS,
self::REASON_INTERNAL_SERVER_ERROR,
self::REASON_SERVICE_UNAVAILABLE,
self::REASON_SHUTDOWN,
];
public const GROUP_INVALID_TOKEN_REASONS = [
self::REASON_BAD_DEVICE_TOKEN,
self::REASON_DEVICE_TOKEN_NOT_FOR_TOPIC,
];
private $reason;
public function __construct(string $reason, $code = 400, Throwable $previous = null)
{
if (!in_array($reason, static::GROUP_VALID_REASONS, true)) {
$reason = static::REASON_UNKNOWN;
}
$this->reason = $reason;
parent::__construct("Request failed with reason: [{$code}]: {$reason}", $code, $previous);
}
public function getReason(): string
{
return $this->reason;
}
public function isInvalidDeviceToken(): bool
{
return in_array($this->reason, static::GROUP_INVALID_TOKEN_REASONS, true);
}
}
......@@ -3,6 +3,9 @@
namespace ricwein\PushNotification\Handler;
use ricwein\PushNotification\Config;
use ricwein\PushNotification\Exceptions\RequestException;
use ricwein\PushNotification\Exceptions\ResponseException;
use ricwein\PushNotification\Exceptions\ResponseReasonException;
use ricwein\PushNotification\Handler;
use ricwein\PushNotification\Message;
use RuntimeException;
......@@ -73,6 +76,17 @@ class APNS extends Handler
$this->timeout = $timeout;
}
public function addDevice(string $token): void
{
if (64 !== $length = strlen($token)) {
throw new RuntimeException("Invalid device-token {$token}, length must be 64 chars but is {$length}.", 500);
}
if (!ctype_xdigit($token)) {
throw new RuntimeException("Invalid device-token {$token}, must be of type hexadecimal but is not.");
}
$this->devices[] = $token;
}
public function send(Message $message): array
{
if (count($this->devices) < 1) {
......@@ -150,20 +164,20 @@ class APNS extends Handler
if ($result === false) {
if (!empty($error)) {
$feedback[$deviceToken] = new RuntimeException("Request failed with: [{$errorCode}]: {$error}", 500);
$feedback[$deviceToken] = new RequestException("Request failed with: [{$errorCode}]: {$error}", 500);
} elseif ($httpStatusCode !== 0) {
$feedback[$deviceToken] = new RuntimeException("Request failed with: HTTP status code {$httpStatusCode}.", 500);
$feedback[$deviceToken] = new RequestException("Request failed with: HTTP status code {$httpStatusCode}.", 500);
} else {
$feedback[$deviceToken] = new RuntimeException("Request failed.", 500);
$feedback[$deviceToken] = new RequestException("Request failed.", 500);
}
continue;
}
$result = @json_decode($result, true);
if (isset($result['reason'])) {
$feedback[$deviceToken] = new RuntimeException("Request failed with: [{$errorCode}]: {$error} - Reason: {$result['reason']}", $httpStatusCode);
$feedback[$deviceToken] = new ResponseReasonException($result['reason'], $httpStatusCode);
} else {
$feedback[$deviceToken] = new RuntimeException("Request failed with: [{$errorCode}]: {$error}", $httpStatusCode);
$feedback[$deviceToken] = new ResponseException("Request failed with: [{$errorCode}]: {$error}", $httpStatusCode);
}
}
......
......@@ -3,6 +3,8 @@
namespace ricwein\PushNotification\Handler;
use ricwein\PushNotification\Config;
use ricwein\PushNotification\Exceptions\RequestException;
use ricwein\PushNotification\Exceptions\ResponseException;
use ricwein\PushNotification\Handler;
use ricwein\PushNotification\Message;
use RuntimeException;
......@@ -53,6 +55,22 @@ class APNSBinary extends Handler
$this->timeout = $timeout;
}
public function addDevice(string $token): void
{
if (64 !== $length = strlen($token)) {
throw new RuntimeException("Invalid device-token {$token}, length must be 64 chars but is {$length}.", 500);
}
if (!ctype_xdigit($token)) {
throw new RuntimeException("Invalid device-token {$token}, must be of type hexadecimal but is not.");
}
$this->devices[] = $token;
}
/**
* @param Message $message
* @return array
* @throws RequestException
*/
public function send(Message $message): array
{
if (count($this->devices) < 1) {
......@@ -70,13 +88,19 @@ class APNSBinary extends Handler
return $this->sendRaw($payload, $message->getPriority());
}
/**
* @param array $payload
* @param int $priority
* @return array
* @throws RequestException
*/
public function sendRaw(array $payload, int $priority = Config::PRIORITY_HIGH): array
{
if (count($this->devices) < 1) {
return [];
}
$arbitrary = ['command' => 1, 'priority' => $priority === Config::PRIORITY_HIGH ? 10 : 5];
$arbitrary = ['command' => 2, 'priority' => $priority === Config::PRIORITY_HIGH ? 10 : 5];
// extract arbitrary settings
foreach (['expire', 'messageID', 'priority', 'command'] as $key) {
......@@ -106,18 +130,18 @@ class APNSBinary extends Handler
);
if (!$stream) {
throw new RuntimeException("Error connecting to server: [{$errno}] {$errstr}", 500);
throw new RequestException("Error connecting to server: [{$errno}] {$errstr}", 500);
}
$content = json_encode($payload, JSON_UNESCAPED_UNICODE);
try {
set_error_handler(static function (int $errno, string $errstr): bool {
set_error_handler(static function (int $errorCode, string $error): bool {
if (0 === error_reporting()) {
// error was suppressed with the @-operator
return false;
}
throw new RuntimeException("Sending to APNS failed: [{$errno}] - {$errstr}");
throw new RequestException("Sending to APNS failed: [{$errorCode}] - {$error}");
});
$feedback = [];
......@@ -133,7 +157,7 @@ class APNSBinary extends Handler
continue;
}
$feedback[$deviceToken] = new RuntimeException("Request failed.", 500);
$feedback[$deviceToken] = new RequestException("Request failed.", 500);
}
$this->devices = [];
......
......@@ -3,6 +3,8 @@
namespace ricwein\PushNotification\Handler;
use ricwein\PushNotification\Config;
use ricwein\PushNotification\Exceptions\RequestException;
use ricwein\PushNotification\Exceptions\ResponseException;
use ricwein\PushNotification\Handler;
use ricwein\PushNotification\Message;
use RuntimeException;
......@@ -97,18 +99,18 @@ class FCM extends Handler
curl_setopt_array($curl, $options);
// execute request
$result = curl_exec($curl);
$response = curl_exec($curl);
$httpStatusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($result === false || 200 !== $httpStatusCode) {
if ($response === false || 200 !== $httpStatusCode) {
$errorCode = curl_errno($curl);
$error = curl_error($curl);
return [new RuntimeException("Request failed with: [{$errorCode}]: {$error}", $httpStatusCode)];
return [new RequestException("Request failed with: [{$errorCode}]: {$error}", $httpStatusCode)];
}
$result = @json_decode($result, true);
if (!isset($result['success']) || (int)$result['success'] !== count($this->devices)) {
return [new RuntimeException("Requests was send, but resulted in an error.", 400)];
$result = @json_decode($response, true);
if ($result === null || !isset($result['success']) || (int)$result['success'] !== count($this->devices)) {
return [new ResponseException("Requests was send, but resulted in an error. Response: {$response}", 400)];
}
$this->devices = [];
......
......@@ -3,6 +3,8 @@
namespace ricwein\PushNotification;
use Exception;
use ricwein\PushNotification\Exceptions\RequestException;
use ricwein\PushNotification\Exceptions\ResponseReasonException;
use Throwable;
class Result
......@@ -17,6 +19,17 @@ class Result
$this->feedback = $feedback;
}
/**
* @return array<Exception|null>
*/
public function getFeedback(): array
{
return $this->feedback;
}
/**
* @return array<Exception>
*/
public function getFailed(): array
{
$failed = [];
......@@ -29,12 +42,39 @@ class Result
}
/**
* @throws Exception
*/
public function throwOnFirstException(): void
{
foreach ($this->feedback as $error) {
if ($error instanceof Exception) {
throw $error;
}
}
}
/**
* @return array<string>
*/
public function getInvalidDeviceTokes(): array
{
$invalidTokens = [];
foreach ($this->feedback as $token => $result) {
if ($result instanceof ResponseReasonException && $result->isInvalidDeviceToken()) {
$invalidTokens[] = $token;
}
}
return $invalidTokens;
}
/**
* @param string $type
* @throws Throwable
*/
public function throwOnFirstError(): void
public function throwOnFirstExceptionOfType(string $type): void
{
foreach ($this->feedback as $error) {
if ($error instanceof Throwable) {
if ($error instanceof $type) {
throw $error;
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment