├── LICENSE ├── composer.json ├── config └── swish.php ├── phpunit.xml └── src ├── Api ├── AbstractResource.php ├── Payments.php ├── Payouts.php └── Refunds.php ├── Callback.php ├── Certificate.php ├── Client.php ├── Error.php ├── Exceptions ├── CallbackDecodingException.php ├── CertificateDecodingException.php ├── ClientException.php ├── InvalidUuidException.php ├── ServerException.php └── ValidationException.php ├── Facades └── Swish.php ├── Payment.php ├── PaymentResult.php ├── Payout.php ├── PayoutResult.php ├── Providers └── SwishServiceProvider.php ├── Refund.php ├── RefundResult.php ├── Resource.php └── Util ├── Crypto.php ├── Id.php ├── Time.php └── Uuid.php /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2025 Marcus Olsson 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "olssonm/swish-php", 3 | "description": "Swish API-wrapper. Compatible with Laravel", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Marcus Olsson", 8 | "email": "contact@marcusolsson.me", 9 | "homepage": "https://marcusolsson.me" 10 | } 11 | ], 12 | "homepage": "https://github.com/olssonm/swish-php", 13 | "require": { 14 | "php": "^8.2", 15 | "guzzlehttp/guzzle": "^7.0", 16 | "nesbot/carbon": "^2 || ^3", 17 | "ramsey/uuid": "^4.2" 18 | }, 19 | "require-dev": { 20 | "ergebnis/composer-normalize": "^2.43", 21 | "orchestra/testbench": "^9.0 || ^10", 22 | "pestphp/pest": "^2.0 || ^3.0", 23 | "phpstan/phpstan": "^1.10", 24 | "phpunit/phpunit": "^10 || ^11.5.3", 25 | "squizlabs/php_codesniffer": "^3.5" 26 | }, 27 | "suggest": { 28 | "illuminate/contracts": "Required to use the Laravel integration (^11.0|^12.0).", 29 | "illuminate/support": "Required to use the Laravel integration (^11.0|^12.0)." 30 | }, 31 | "minimum-stability": "dev", 32 | "prefer-stable": true, 33 | "autoload": { 34 | "psr-4": { 35 | "Olssonm\\Swish\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Olssonm\\Swish\\Test\\": "tests" 41 | } 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "ergebnis/composer-normalize": true, 46 | "pestphp/pest-plugin": true 47 | } 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-main": "3.0.x-dev" 52 | }, 53 | "laravel": { 54 | "aliases": { 55 | "Swish": "Olssonm\\Swish\\Facades\\Swish" 56 | }, 57 | "providers": [ 58 | "Olssonm\\Swish\\Providers\\SwishServiceProvider" 59 | ] 60 | } 61 | }, 62 | "scripts": { 63 | "coverage": "XDEBUG_MODE=coverage vendor/bin/pest --coverage", 64 | "phpfix": "vendor/bin/phpcbf --standard=\"PSR12\" ./src", 65 | "phpsniff": "vendor/bin/phpcs --standard=\"PSR12\" ./src", 66 | "phpstan": "./vendor/bin/phpstan", 67 | "test": "./vendor/bin/pest" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/swish.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'client' => env('SWISH_CLIENT_CERTIFICATE_PATH'), 6 | 'password' => env('SWISH_CLIENT_CERTIFICATE_PASSWORD'), 7 | 'root' => env('SWISH_ROOT_CERTIFICATE_PATH', true), 8 | 'signing' => env('SWISH_SIGNING_CERTIFICATE_PATH'), // Optional, used for payouts 9 | 'signing_password' => env('SWISH_SIGNING_CERTIFICATE_PASSWORD'), // Optional, used for payouts 10 | ], 11 | 'endpoint' => env('SWISH_URL', \Olssonm\Swish\Client::PRODUCTION_ENDPOINT), 12 | ]; 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | 15 | 16 | ./src 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Api/AbstractResource.php: -------------------------------------------------------------------------------- 1 | client = $client; 26 | 27 | if ($swish) { 28 | $this->swish = $swish; 29 | } 30 | } 31 | 32 | /** 33 | * Retrieve resource 34 | * 35 | * @param $transaction 36 | * @return mixed 37 | */ 38 | abstract public function get($transaction); // @phpstan-ignore-line 39 | 40 | /** 41 | * Create resource 42 | * 43 | * @param $transaction 44 | * @return mixed 45 | */ 46 | abstract public function create($transaction); // @phpstan-ignore-line 47 | 48 | /** 49 | * Cancel transaction 50 | * 51 | * @param $transaction 52 | * @return mixed 53 | */ 54 | abstract public function cancel($transaction); // @phpstan-ignore-line 55 | 56 | /** 57 | * Main API caller 58 | * 59 | * @param string $verb 60 | * @param string $uri 61 | * @param array $headers 62 | * @param string|null $payload 63 | * @return ResponseInterface 64 | * @throws ClientException|ServerException|ValidationException 65 | */ 66 | protected function request( 67 | string $verb, 68 | string $uri, 69 | array $headers = [], 70 | string|null $payload = null 71 | ): ResponseInterface { 72 | $request = new Psr7Request( 73 | $verb, 74 | $uri, 75 | array_merge([ 76 | 'Content-Type' => 'application/json', 77 | 'Accept' => 'application/json' 78 | ], $headers), 79 | $payload 80 | ); 81 | 82 | $response = $this->client->send($request); 83 | 84 | $status = $response->getStatusCode(); 85 | $level = (int) \floor($status / 100); 86 | 87 | switch (true) { 88 | case $status == 403: 89 | // No break 90 | case $status == 422: 91 | $this->triggerException( 92 | ValidationException::class, 93 | 'Validation error', 94 | $request, 95 | $response 96 | ); 97 | // No break 98 | case $level == 4: 99 | $this->triggerException( 100 | ClientException::class, 101 | 'Client error', 102 | $request, 103 | $response 104 | ); 105 | // No break 106 | case $level == 5: 107 | $this->triggerException( 108 | ServerException::class, 109 | 'Server error', 110 | $request, 111 | $response 112 | ); 113 | } 114 | 115 | return $response; 116 | } 117 | 118 | /** 119 | * Trigger a request exception 120 | * 121 | * @param string $class 122 | * @param string $label 123 | * @param RequestInterface $request 124 | * @param ResponseInterface $response 125 | * @return void 126 | */ 127 | protected function triggerException( 128 | string $class, 129 | string $label, 130 | RequestInterface $request, 131 | ResponseInterface $response 132 | ): void { 133 | $message = \sprintf( 134 | '%s: `%s %s` resulted in a `%s %s` response', 135 | $label, 136 | $request->getMethod(), 137 | $request->getUri()->__toString(), 138 | $response->getStatusCode(), 139 | $response->getReasonPhrase() 140 | ); 141 | 142 | /** 143 | * @var \Exception 144 | */ 145 | throw new $class( 146 | $message, 147 | $request, 148 | $response 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Api/Payments.php: -------------------------------------------------------------------------------- 1 | request('GET', sprintf('v1/paymentrequests/%s', $payment->id)); 23 | 24 | return new Payment(json_decode((string) $response->getBody(), true)); 25 | } 26 | 27 | /** 28 | * Create a payment. 29 | * 30 | * @param Payment $payment 31 | * @return PaymentResult 32 | */ 33 | public function create($payment): PaymentResult 34 | { 35 | $response = $this->request( 36 | 'PUT', 37 | sprintf('v2/paymentrequests/%s', $payment->id), 38 | [], 39 | (string) json_encode($payment) 40 | ); 41 | 42 | $location = $response->getHeaderLine('Location'); 43 | $token = $response->getHeaderLine('PaymentRequestToken'); 44 | 45 | return new PaymentResult([ 46 | 'id' => Id::parse($response), 47 | 'location' => strlen($location) > 0 ? $location : null, 48 | 'paymentRequestToken' => strlen($token) > 0 ? $token : null, 49 | ]); 50 | } 51 | 52 | /** 53 | * Cancel a payment. 54 | * 55 | * @param Payment $payment 56 | * @return Payment 57 | */ 58 | public function cancel($payment): Payment 59 | { 60 | $response = $this->request('PATCH', sprintf('v1/paymentrequests/%s', $payment->id), [ 61 | 'Content-Type' => 'application/json-patch+json', 62 | ], (string) json_encode([[ 63 | 'op' => 'replace', 64 | 'path' => '/status', 65 | 'value' => 'cancelled', 66 | ]])); 67 | 68 | return new Payment(json_decode((string) $response->getBody(), true)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Api/Payouts.php: -------------------------------------------------------------------------------- 1 | request('GET', sprintf('v1/payouts/%s', $payout->payoutInstructionUUID)); 26 | 27 | return new Payout(json_decode((string) $response->getBody(), true)); 28 | } 29 | 30 | /** 31 | * Create a payment. 32 | * 33 | * @param Payout $payout 34 | * @return PayoutResult 35 | */ 36 | public function create($payout): PayoutResult 37 | { 38 | $certificate = $this->swish->getCertificate()->getSigningCertificate(); 39 | $signature = Crypto::hashAndSign($payout, $certificate); 40 | 41 | $response = $this->request('POST', 'v1/payouts', [], (string) json_encode( 42 | [ 43 | 'payload' => $payout, 44 | 'callbackUrl' => $payout->callbackUrl, 45 | 'signature' => $signature, 46 | ] 47 | )); 48 | 49 | $location = $response->getHeaderLine('Location'); 50 | 51 | return new PayoutResult([ 52 | 'payoutInstructionUUID' => Id::parse($response), 53 | 'location' => strlen($location) > 0 ? $location : null 54 | ]); 55 | } 56 | 57 | /** 58 | * Cancel a payout. 59 | * 60 | * @param Payout $transaction 61 | * @throws \BadMethodCallException 62 | */ 63 | public function cancel($transaction): void 64 | { 65 | throw new \BadMethodCallException('Payouts can not be cancelled.'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Api/Refunds.php: -------------------------------------------------------------------------------- 1 | request('GET', sprintf('v1/refunds/%s', $refund->id)); 23 | 24 | return new Refund(json_decode((string) $response->getBody(), true)); 25 | } 26 | 27 | /** 28 | * Create a refund. 29 | * 30 | * @param Refund $refund 31 | * @return RefundResult 32 | */ 33 | public function create($refund): RefundResult 34 | { 35 | $response = $this->request('PUT', sprintf('v2/refunds/%s', $refund->id), [], (string) json_encode($refund)); 36 | 37 | $location = $response->getHeaderLine('Location'); 38 | 39 | return new RefundResult([ 40 | 'id' => Id::parse($response), 41 | 'location' => strlen($location) > 0 ? $location : null, 42 | ]); 43 | } 44 | 45 | /** 46 | * Cancel a refund. 47 | * 48 | * @param Refund $transaction 49 | * @throws \BadMethodCallException 50 | */ 51 | public function cancel($transaction): void 52 | { 53 | throw new \BadMethodCallException('Refunds can not be cancelled.'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Callback.php: -------------------------------------------------------------------------------- 1 | client = $clientPath; 34 | $this->passphrase = $passphrase; 35 | $this->root = $rootPath; 36 | $this->signing = $signingPath; 37 | $this->signingPassphrase = $signingPassphrase; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function getClientCertificate(): array 44 | { 45 | return [ 46 | $this->client, 47 | $this->passphrase, 48 | ]; 49 | } 50 | 51 | /** 52 | * @return bool|string 53 | */ 54 | public function getRootCertificate(): bool|string 55 | { 56 | return $this->root; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getSigningCertificate(): array 63 | { 64 | return [ 65 | $this->signing, 66 | $this->signingPassphrase, 67 | ]; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getSerial(): string 74 | { 75 | try { 76 | $content = file_get_contents($this->signing); // @phpstan-ignore argument.type 77 | $details = openssl_x509_read($content); // @phpstan-ignore argument.type 78 | $results = openssl_x509_parse($details); // @phpstan-ignore argument.type 79 | $serial = $results['serialNumberHex']; // @phpstan-ignore offsetAccess.nonOffsetAccessible 80 | } catch (\Throwable $th) { 81 | throw new CertificateDecodingException( 82 | 'Could notretrieve the serial number for the certificate. Please check your path and passphrase.', 83 | 0, 84 | $th 85 | ); 86 | } 87 | 88 | return $serial; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $history = []; 29 | 30 | public const PRODUCTION_ENDPOINT = 'https://cpc.getswish.net/swish-cpcapi/api/'; 31 | 32 | public const TEST_ENDPOINT = 'https://mss.cpc.getswish.net/swish-cpcapi/api/'; 33 | 34 | public const SANDBOX_ENDPOINT = 'https://staging.getswish.pub.tds.tieto.com/swish-cpcapi/api/'; 35 | 36 | protected ClientInterface $client; 37 | 38 | public function __construct( 39 | ?Certificate $certificate = null, 40 | string $endpoint = self::PRODUCTION_ENDPOINT, 41 | ?ClientInterface $client = null 42 | ) { 43 | $this->setup($certificate, $endpoint, $client); 44 | } 45 | 46 | public function setup( 47 | ?Certificate $certificate = null, 48 | string $endpoint = self::PRODUCTION_ENDPOINT, 49 | ?ClientInterface $client = null 50 | ): void { 51 | 52 | if ($certificate) { 53 | $this->setCertificate($certificate); 54 | } 55 | 56 | $handler = new HandlerStack(); 57 | $handler->setHandler(new CurlHandler()); 58 | $handler->push(Middleware::history($this->history)); 59 | 60 | $this->client = $client ?? new GuzzleHttpClient([ 61 | 'handler' => $handler, 62 | 'curl' => [ 63 | CURLOPT_TCP_KEEPALIVE => 1, 64 | CURLOPT_TCP_KEEPIDLE => 10, 65 | CURLOPT_TIMEOUT => 0, 66 | CURLOPT_CONNECTTIMEOUT => 20, 67 | ], 68 | 'verify' => $certificate?->getRootCertificate(), 69 | 'cert' => $certificate?->getClientCertificate(), 70 | 'base_uri' => $endpoint, 71 | 'http_errors' => false, 72 | ]); 73 | } 74 | 75 | /** 76 | * Return the clients call-history 77 | * 78 | * @return array 79 | */ 80 | public function getHistory(): array 81 | { 82 | return $this->history; 83 | } 84 | 85 | /** 86 | * Return the clients call-history 87 | * 88 | * @return Certificate 89 | */ 90 | public function getCertificate(): Certificate 91 | { 92 | return $this->certificate; 93 | } 94 | 95 | /** 96 | * Set the certificate 97 | * 98 | * @param Certificate $certificate 99 | * @return void 100 | */ 101 | public function setCertificate(Certificate $certificate): void 102 | { 103 | $this->certificate = $certificate; 104 | } 105 | 106 | /** 107 | * @param array $args 108 | */ 109 | public function __call(string $method, array $args): mixed 110 | { 111 | if ( 112 | !is_object($args[0]) || 113 | ( 114 | (get_class($args[0]) != Payment::class) && 115 | (get_class($args[0]) != Refund::class) && 116 | (get_class($args[0]) != Payout::class) 117 | ) 118 | ) { 119 | throw new InvalidArgumentException( 120 | 'Only Payment-, Payout- and Refund-objects are allowed as first argument' 121 | ); 122 | } 123 | 124 | switch (get_class($args[0])) { 125 | case Payment::class: 126 | $class = new Payments($this->client); 127 | break; 128 | 129 | case Refund::class: 130 | $class = new Refunds($this->client); 131 | break; 132 | 133 | case Payout::class: 134 | $class = new Payouts($this->client, $this); 135 | break; 136 | } 137 | 138 | // @phpstan-ignore-next-line 139 | return call_user_func_array([$class, $method], $args); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Error.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $errors = []; 17 | 18 | /** 19 | * Undocumented function 20 | * 21 | * @param array $handlerContext 22 | */ 23 | public function __construct( 24 | string $message, 25 | RequestInterface $request, 26 | ?ResponseInterface $response = null, 27 | ?Throwable $previous = null, 28 | array $handlerContext = [] 29 | ) { 30 | 31 | $data = json_decode((string) $response?->getBody()->getContents()); 32 | 33 | if (is_array($data)) { 34 | foreach ($data as $error) { 35 | $this->errors[] = new Error((array) $error); 36 | } 37 | } else { 38 | $this->errors[] = new Error([ 39 | 'errorCode' => $data->errorCode, 40 | 'errorMessage' => $data->errorMessage, 41 | 'additionalInformation' => $data->additionalInformation ?? null, 42 | ]); 43 | } 44 | 45 | parent::__construct($message, $request, $response, $previous, $handlerContext); 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getErrors(): array 52 | { 53 | return $this->errors; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Facades/Swish.php: -------------------------------------------------------------------------------- 1 | $attributes 29 | */ 30 | public function __construct(array $attributes = []) 31 | { 32 | parent::__construct($attributes); 33 | $this->id = $this->id ?? Uuid::make(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/PaymentResult.php: -------------------------------------------------------------------------------- 1 | $attributes 31 | */ 32 | public function __construct(array $attributes = []) 33 | { 34 | foreach ($attributes as $key => $value) { 35 | $this->{$key} = $value; 36 | } 37 | 38 | // Assume some default details 39 | $this->payoutInstructionUUID = $this->payoutInstructionUUID ?? Uuid::make(); 40 | $this->currency = $this->currency ?? 'SEK'; 41 | $this->payoutType = $this->payoutType ?? 'PAYOUT'; 42 | } 43 | 44 | public function __get(string $key): mixed 45 | { 46 | if (property_exists($this, $key)) { 47 | return $this->{$key}; 48 | } 49 | 50 | return parent::__get($key); 51 | } 52 | 53 | public function __set(string $key, mixed $value) 54 | { 55 | if (property_exists($this, $key)) { 56 | $this->{$key} = $value; 57 | return; 58 | } 59 | 60 | parent::__set($key, $value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/PayoutResult.php: -------------------------------------------------------------------------------- 1 | publishes([$source => config_path('swish.php')], 'config'); 18 | 19 | $this->mergeConfigFrom($source, 'swish'); 20 | 21 | $this->app->singleton('swish', function (Container $app): Client { 22 | /** @var \Illuminate\Config\Repository $config */ 23 | $config = $app->get('config'); 24 | 25 | /** @var \Illuminate\Filesystem\FilesystemManager $storage */ 26 | $storage = $app->get('filesystem'); 27 | 28 | $certificate = new Certificate( 29 | clientPath: $this->resolvePath($storage, $config->get('swish.certificates.client')), 30 | passphrase: $config->get('swish.certificates.password'), 31 | rootPath: $config->get('swish.certificates.root') === true || $config->get('swish.certificates.root') === false 32 | ? $config->get('swish.certificates.root') 33 | : $this->resolvePath($storage, $config->get('swish.certificates.root')), 34 | signingPath: $this->resolvePath($storage, $config->get('swish.certificates.signing')), 35 | signingPassphrase: $config->get('swish.certificates.signing_password') 36 | ); 37 | 38 | return new Client($certificate, $config->get('swish.endpoint')); 39 | }); 40 | 41 | $this->app->alias('swish', Client::class); 42 | } 43 | 44 | private function resolvePath(FilesystemManager $storage, ?string $path): string 45 | { 46 | if (empty($path)) { 47 | return ''; 48 | } 49 | 50 | return $this->isAbsolutePath($path) ? $path : $storage->path($path); 51 | } 52 | 53 | private function isAbsolutePath(string $path): bool 54 | { 55 | return $path !== '' && ($path[0] === '/' || $path[0] === '\\' || (strlen($path) > 3 && ctype_alpha($path[0]) && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/'))); 56 | } 57 | 58 | /** @return array */ 59 | public function provides(): array 60 | { 61 | return ['swish']; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Refund.php: -------------------------------------------------------------------------------- 1 | $attributes 28 | */ 29 | public function __construct(array $attributes = []) 30 | { 31 | parent::__construct($attributes); 32 | $this->id = $this->id ?? Uuid::make(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/RefundResult.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | protected array $attributes; 14 | 15 | /** 16 | * @param array $attributes 17 | */ 18 | public function __construct(array $attributes = []) 19 | { 20 | $this->attributes = $attributes; 21 | } 22 | 23 | public function __set(string $key, mixed $value) 24 | { 25 | if (in_array($key, ['id', 'instructionUUID'])) { 26 | if (!Uuid::validate($value)) { 27 | throw new InvalidUuidException(); 28 | } 29 | } 30 | 31 | $this->attributes[$key] = $value; 32 | } 33 | 34 | public function __get(string $key): mixed 35 | { 36 | return $this->attributes[$key]; 37 | } 38 | 39 | public function __isset(string $key): bool 40 | { 41 | return isset($this->attributes[$key]); 42 | } 43 | 44 | #[\ReturnTypeWillChange] 45 | public function offsetSet($key, $value): void 46 | { 47 | $this->{$key} = $value; 48 | } 49 | 50 | #[\ReturnTypeWillChange] 51 | public function offsetExists($key): bool 52 | { 53 | return \array_key_exists($key, $this->attributes); 54 | } 55 | 56 | #[\ReturnTypeWillChange] 57 | public function offsetUnset($key): void 58 | { 59 | unset($this->attributes[$key]); 60 | } 61 | 62 | #[\ReturnTypeWillChange] 63 | public function offsetGet($key): mixed 64 | { 65 | return \array_key_exists($key, $this->attributes) ? $this->attributes[$key] : null; 66 | } 67 | 68 | #[\ReturnTypeWillChange] 69 | public function count(): int 70 | { 71 | return \count($this->attributes); 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | #[\ReturnTypeWillChange] 78 | public function jsonSerialize(): array 79 | { 80 | return $this->toArray(); 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function toArray(): array 87 | { 88 | return $this->attributes; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Util/Crypto.php: -------------------------------------------------------------------------------- 1 | $certificate 61 | * @return string 62 | */ 63 | public static function hashAndSign(ArrayAccess $payload, array $certificate): string 64 | { 65 | $data = json_encode($payload); 66 | 67 | if (!$data || !$certificate) { 68 | throw new CertificateDecodingException('Failed to encode payload'); 69 | } 70 | 71 | $hash = self::hash($data); 72 | $signature = self::sign($hash, $certificate[0] ?? '', $certificate[1]); 73 | 74 | return base64_encode($signature); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Util/Id.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Location'), PHP_URL_PATH); 18 | return is_string($url) ? pathinfo($url, PATHINFO_BASENAME) : null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Util/Time.php: -------------------------------------------------------------------------------- 1 | toIso8601String(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Util/Uuid.php: -------------------------------------------------------------------------------- 1 | getHex()->toString(), 'UTF-8'); 59 | default: 60 | return $uuid; 61 | } 62 | } 63 | } 64 | --------------------------------------------------------------------------------