diff --git a/README.md b/README.md index b68d450..b982796 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ # SparkPost PHP Library +## This fork is maintained internally due to inactivity in the upstream repository. Only used within Lightfoot projects. + [![Travis CI](https://travis-ci.org/SparkPost/php-sparkpost.svg?branch=master)](https://travis-ci.org/SparkPost/php-sparkpost) [![Coverage Status](https://coveralls.io/repos/SparkPost/php-sparkpost/badge.svg?branch=master&service=github)](https://coveralls.io/github/SparkPost/php-sparkpost?branch=master) [![Downloads](https://img.shields.io/packagist/dt/sparkpost/sparkpost.svg?maxAge=3600)](https://packagist.org/packages/sparkpost/sparkpost) diff --git a/composer.json b/composer.json index 1736421..61ede6d 100644 --- a/composer.json +++ b/composer.json @@ -8,24 +8,33 @@ } ], "minimum-stability": "stable", + "config": { + "allow-plugins": { + "php-http/discovery": true + } + }, "scripts": { "test": "XDEBUG_MODE=coverage ./vendor/bin/phpunit", "fix-style": "php-cs-fixer fix ." }, "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "php-http/httplug": "^1.0 || ^2.0", + "psr/http-factory": "^1.0", + "nyholm/psr7": "^1.0", "php-http/message": "^1.0", "php-http/client-implementation": "^1.0", - "php-http/discovery": "^1.0" + "php-http/discovery": "^1.0", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^8.0 || ^9.0", - "php-http/guzzle6-adapter": "^1.0", + "rector/rector": "^1.0", + "php-http/guzzle7-adapter": "^1.0", "mockery/mockery": "^1.3", "nyholm/nsa": "^1.0", "php-coveralls/php-coveralls": "^2.4", - "friendsofphp/php-cs-fixer": "^2.18" + "friendsofphp/php-cs-fixer": "^3.9" }, "autoload": { "psr-4": { diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 688d53e..6c8c4f5 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -1,3 +1,3 @@ getRequest()); echo "Response:\n"; - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { echo "Request:\n"; print_r($e->getRequest()); echo "Exception:\n"; - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/message-events/get_message_events.php b/examples/message-events/get_message_events.php index 92bc70b..40ca911 100644 --- a/examples/message-events/get_message_events.php +++ b/examples/message-events/get_message_events.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -20,9 +20,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/message-events/get_message_events_with_retry_logic.php b/examples/message-events/get_message_events_with_retry_logic.php index 82efd2a..a4d7ee2 100644 --- a/examples/message-events/get_message_events_with_retry_logic.php +++ b/examples/message-events/get_message_events_with_retry_logic.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -23,11 +23,11 @@ */ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; if ($e->getCode() >= 500 && $e->getCode() <= 599) { echo "Wow, this failed epically"; diff --git a/examples/templates/create_template.php b/examples/templates/create_template.php index 1d697d5..0ce0d0b 100644 --- a/examples/templates/create_template.php +++ b/examples/templates/create_template.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -22,16 +22,16 @@ // Valid short template content examples $plain_text = 'Write your text message part here.'; -$html = <<

Write your HTML message part here

-HTML; +HTML_WRAP; -$amp_html = << @@ -43,25 +43,25 @@ Hello World! Let's get started using AMP HTML together! -HTML; +HTML_WRAP; $promise = $sparky->request('POST', 'templates', [ - 'name' => $template_name, - 'id' => $template_id, - 'content' => [ - 'from' => "from@$sending_domain", - 'subject' => 'Your Subject', - 'text' => $plain_text, - 'html' => $html, - 'amp_html' => $amp_html, - ], + 'name' => $template_name, + 'id' => $template_id, + 'content' => [ + 'from' => "from@$sending_domain", + 'subject' => 'Your Subject', + 'text' => $plain_text, + 'html' => $html, + 'amp_html' => $amp_html, + ], ]); try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/templates/delete_template.php b/examples/templates/delete_template.php index 609e67e..34e66e7 100644 --- a/examples/templates/delete_template.php +++ b/examples/templates/delete_template.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -19,9 +19,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/templates/get_all_templates.php b/examples/templates/get_all_templates.php index 87a5b4d..722126d 100644 --- a/examples/templates/get_all_templates.php +++ b/examples/templates/get_all_templates.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -17,9 +17,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/templates/get_template.php b/examples/templates/get_template.php index 3dd8849..647e022 100644 --- a/examples/templates/get_template.php +++ b/examples/templates/get_template.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -19,9 +19,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/templates/preview_template.php b/examples/templates/preview_template.php index 51675a2..8af61d9 100644 --- a/examples/templates/preview_template.php +++ b/examples/templates/preview_template.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -23,9 +23,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/templates/update_template.php b/examples/templates/update_template.php index 2f79cfd..83a1c05 100644 --- a/examples/templates/update_template.php +++ b/examples/templates/update_template.php @@ -2,7 +2,7 @@ namespace Examples\Templates; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -23,9 +23,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/create_transmission.php b/examples/transmissions/create_transmission.php index 6204ff8..a137f9e 100644 --- a/examples/transmissions/create_transmission.php +++ b/examples/transmissions/create_transmission.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -40,9 +40,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/create_transmission_with_attachment.php b/examples/transmissions/create_transmission_with_attachment.php index b97dd1d..2d85e5d 100644 --- a/examples/transmissions/create_transmission_with_attachment.php +++ b/examples/transmissions/create_transmission_with_attachment.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -13,10 +13,10 @@ // In these examples, fetch API key from environment variable $sparky = new SparkPost($httpClient, ["key" => getenv('SPARKPOST_API_KEY')]); -$filePath = dirname(__FILE__).'/'; +$filePath = __DIR__ . '/'; $fileName = 'sparkpost.png'; -$fileType = mime_content_type($filePath.$fileName); -$fileData = base64_encode(file_get_contents($filePath.$fileName)); +$fileType = mime_content_type($filePath . $fileName); +$fileData = base64_encode(file_get_contents($filePath . $fileName)); // put your own sending domain and test recipient address here $sending_domain = "steve2-test.trymsys.net"; @@ -52,9 +52,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/create_transmission_with_cc_and_bcc.php b/examples/transmissions/create_transmission_with_cc_and_bcc.php index c49ab0c..d7bdb11 100644 --- a/examples/transmissions/create_transmission_with_cc_and_bcc.php +++ b/examples/transmissions/create_transmission_with_cc_and_bcc.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -58,9 +58,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/create_transmission_with_recipient_list.php b/examples/transmissions/create_transmission_with_recipient_list.php index b95a994..5b8eec3 100644 --- a/examples/transmissions/create_transmission_with_recipient_list.php +++ b/examples/transmissions/create_transmission_with_recipient_list.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -35,9 +35,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/create_transmission_with_template.php b/examples/transmissions/create_transmission_with_template.php index 66de0b9..60a8d5a 100644 --- a/examples/transmissions/create_transmission_with_template.php +++ b/examples/transmissions/create_transmission_with_template.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -34,9 +34,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/examples/transmissions/delete_transmission.php b/examples/transmissions/delete_transmission.php index e7fb65a..da51da9 100644 --- a/examples/transmissions/delete_transmission.php +++ b/examples/transmissions/delete_transmission.php @@ -2,7 +2,7 @@ namespace Examples\Transmissions; -require dirname(__FILE__).'/../bootstrap.php'; +require __DIR__ . '/../bootstrap.php'; use SparkPost\SparkPost; use GuzzleHttp\Client; @@ -20,9 +20,9 @@ try { $response = $promise->wait(); - echo $response->getStatusCode()."\n"; - print_r($response->getBody())."\n"; + echo $response->getStatusCode() . "\n"; + print_r($response->getBody()) . "\n"; } catch (\Exception $e) { - echo $e->getCode()."\n"; - echo $e->getMessage()."\n"; + echo $e->getCode() . "\n"; + echo $e->getMessage() . "\n"; } diff --git a/lib/SparkPost/ResourceBase.php b/lib/SparkPost/ResourceBase.php index 3620192..9197935 100644 --- a/lib/SparkPost/ResourceBase.php +++ b/lib/SparkPost/ResourceBase.php @@ -2,6 +2,9 @@ namespace SparkPost; +use Http\Client\Exception; +use Psr\Http\Client\ClientExceptionInterface; + /** * Class ResourceBase. */ @@ -10,20 +13,20 @@ class ResourceBase /** * SparkPost object used to make requests. */ - protected $sparkpost; + protected SparkPost $sparkpost; /** * The api endpoint that gets prepended to all requests send through this resource. */ - protected $endpoint; + protected string $endpoint; /** * Sets up the Resource. * * @param SparkPost $sparkpost - the sparkpost instance that this resource is attached to - * @param string $endpoint - the endpoint that this resource wraps + * @param string $endpoint - the endpoint that this resource wraps */ - public function __construct(SparkPost $sparkpost, $endpoint) + public function __construct(SparkPost $sparkpost, string $endpoint) { $this->sparkpost = $sparkpost; $this->endpoint = $endpoint; @@ -32,9 +35,11 @@ public function __construct(SparkPost $sparkpost, $endpoint) /** * Sends get request to API at the set endpoint. * + * @return SparkPostPromise|SparkPostResponse + * @throws SparkPostException|Exception|ClientExceptionInterface * @see SparkPost->request() */ - public function get($uri = '', $payload = [], $headers = []) + public function get($uri = '', array $payload = [], array $headers = []) { return $this->request('GET', $uri, $payload, $headers); } @@ -42,9 +47,11 @@ public function get($uri = '', $payload = [], $headers = []) /** * Sends put request to API at the set endpoint. * + * @return SparkPostPromise|SparkPostResponse + * @throws SparkPostException|Exception|ClientExceptionInterface * @see SparkPost->request() */ - public function put($uri = '', $payload = [], $headers = []) + public function put($uri = '', array $payload = [], array $headers = []) { return $this->request('PUT', $uri, $payload, $headers); } @@ -52,9 +59,12 @@ public function put($uri = '', $payload = [], $headers = []) /** * Sends post request to API at the set endpoint. * + * @return SparkPostPromise|SparkPostResponse + * @throws SparkPostException + * @throws Exception|ClientExceptionInterface * @see SparkPost->request() */ - public function post($payload = [], $headers = []) + public function post(array $payload = [], array $headers = []) { return $this->request('POST', '', $payload, $headers); } @@ -62,9 +72,11 @@ public function post($payload = [], $headers = []) /** * Sends delete request to API at the set endpoint. * + * @return SparkPostPromise|SparkPostResponse + * @throws SparkPostException|Exception|ClientExceptionInterface * @see SparkPost->request() */ - public function delete($uri = '', $payload = [], $headers = []) + public function delete($uri = '', array $payload = [], array $headers = []) { return $this->request('DELETE', $uri, $payload, $headers); } @@ -72,11 +84,13 @@ public function delete($uri = '', $payload = [], $headers = []) /** * Sends requests to SparkPost object to the resource endpoint. * + * @return SparkPostPromise|SparkPostResponse depending on sync or async request + * @throws SparkPostException + * @throws Exception|ClientExceptionInterface * @see SparkPost->request() * - * @return SparkPostPromise or SparkPostResponse depending on sync or async request */ - public function request($method = 'GET', $uri = '', $payload = [], $headers = []) + public function request(string $method = 'GET', $uri = '', array $payload = [], array $headers = []) { if (is_array($uri)) { $headers = $payload; @@ -84,7 +98,7 @@ public function request($method = 'GET', $uri = '', $payload = [], $headers = [] $uri = ''; } - $uri = $this->endpoint.'/'.$uri; + $uri = $this->endpoint . '/' . $uri; return $this->sparkpost->request($method, $uri, $payload, $headers); } diff --git a/lib/SparkPost/SparkPost.php b/lib/SparkPost/SparkPost.php index 140caf7..aa04c4b 100644 --- a/lib/SparkPost/SparkPost.php +++ b/lib/SparkPost/SparkPost.php @@ -2,38 +2,36 @@ namespace SparkPost; -use Http\Client\HttpClient; +use Http\Client\Exception; use Http\Client\HttpAsyncClient; -use Http\Discovery\MessageFactoryDiscovery; -use Http\Message\RequestFactory; +use Http\Discovery\Psr17FactoryDiscovery; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamFactoryInterface; class SparkPost { - /** - * @var string Library version, used for setting User-Agent - */ - private $version = '2.3.0'; + + private string $version = '2.3.0'; /** - * @var HttpClient|HttpAsyncClient used to make requests + * @var ClientInterface|HttpAsyncClient used to make requests */ private $httpClient; - /** - * @var RequestFactory - */ - private $messageFactory; + private RequestFactoryInterface $requestFactory; - /** - * @var array Options for requests - */ - private $options; + private StreamFactoryInterface $streamFactory; + + private array $options; /** * Default options for requests that can be overridden with the setOptions function. */ - private static $defaultOptions = [ + private static array $defaultOptions = [ 'host' => 'api.sparkpost.com', 'protocol' => 'https', 'port' => 443, @@ -44,18 +42,16 @@ class SparkPost 'retries' => 0 ]; - /** - * @var Transmission Instance of Transmission class - */ - public $transmissions; + public Transmission $transmissions; /** * Sets up the SparkPost instance. * - * @param HttpClient $httpClient - An httplug client or adapter - * @param array $options - An array to overide default options or a string to be used as an API key + * @param ClientInterface $httpClient - An httplug client or adapter + * @param array $options - An array to overide default options or a string to be used as an API key + * @throws \Exception */ - public function __construct($httpClient, array $options) + public function __construct(ClientInterface $httpClient, array $options) { $this->setOptions($options); $this->setHttpClient($httpClient); @@ -67,12 +63,14 @@ public function __construct($httpClient, array $options) * * @param string $method * @param string $uri - * @param array $payload - either used as the request body or url query params - * @param array $headers + * @param array $payload - either used as the request body or url query params + * @param array $headers * * @return SparkPostPromise|SparkPostResponse Promise or Response depending on sync or async request + * @throws SparkPostException + * @throws \Exception */ - public function request($method = 'GET', $uri = '', $payload = [], $headers = []) + public function request(string $method = 'GET', string $uri = '', array $payload = [], array $headers = []) { if ($this->options['async'] === true) { return $this->asyncRequest($method, $uri, $payload, $headers); @@ -86,37 +84,41 @@ public function request($method = 'GET', $uri = '', $payload = [], $headers = [] * * @param string $method * @param string $uri - * @param array $payload - * @param array $headers + * @param array $payload + * @param array $headers * * @return SparkPostResponse * * @throws SparkPostException + * @throws \Exception */ - public function syncRequest($method = 'GET', $uri = '', $payload = [], $headers = []) + public function syncRequest(string $method = 'GET', string $uri = '', array $payload = [], array $headers = []): SparkPostResponse { $requestValues = $this->buildRequestValues($method, $uri, $payload, $headers); - $request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues); + $request = call_user_func_array([$this, 'buildRequestInstance'], $requestValues); $retries = $this->options['retries']; try { - if ($retries > 0) { - $resp = $this->syncReqWithRetry($request, $retries); - } else { - $resp = $this->httpClient->sendRequest($request); - } + $resp = $retries > 0 + ? $this->syncReqWithRetry($request, $retries) + : $this->httpClient->sendRequest( + $request + ); return new SparkPostResponse($resp, $this->ifDebug($requestValues)); - } catch (\Exception $exception) { + } catch (\Throwable $exception) { throw new SparkPostException($exception, $this->ifDebug($requestValues)); } } - private function syncReqWithRetry($request, $retries) + /** + * @throws Exception|ClientExceptionInterface + */ + private function syncReqWithRetry( RequestInterface $request, int $retries): ResponseInterface { $resp = $this->httpClient->sendRequest($request); $status = $resp->getStatusCode(); if ($status >= 500 && $status <= 599 && $retries > 0) { - return $this->syncReqWithRetry($request, $retries-1); + return $this->syncReqWithRetry($request, $retries - 1); } return $resp; } @@ -126,34 +128,49 @@ private function syncReqWithRetry($request, $retries) * * @param string $method * @param string $uri - * @param array $payload - * @param array $headers + * @param array $payload + * @param array $headers * * @return SparkPostPromise + * @throws \Exception */ - public function asyncRequest($method = 'GET', $uri = '', $payload = [], $headers = []) - { + public function asyncRequest( + string $method = 'GET', + string $uri = '', + array $payload = [], + array $headers = [] + ): SparkPostPromise { if ($this->httpClient instanceof HttpAsyncClient) { $requestValues = $this->buildRequestValues($method, $uri, $payload, $headers); - $request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues); + $request = call_user_func_array([$this, 'buildRequestInstance'], $requestValues); $retries = $this->options['retries']; if ($retries > 0) { - return new SparkPostPromise($this->asyncReqWithRetry($request, $retries), $this->ifDebug($requestValues)); + return new SparkPostPromise( + $this->asyncReqWithRetry($request, $retries), $this->ifDebug($requestValues) + ); } else { - return new SparkPostPromise($this->httpClient->sendAsyncRequest($request), $this->ifDebug($requestValues)); + return new SparkPostPromise( + $this->httpClient->sendAsyncRequest($request), + $this->ifDebug($requestValues) + ); } } else { - throw new \Exception('Your http client does not support asynchronous requests. Please use a different client or use synchronous requests.'); + throw new \Exception( + 'Your http client does not support asynchronous requests. Please use a different client or use synchronous requests.' + ); } } - private function asyncReqWithRetry($request, $retries) + /** + * @throws \Exception + */ + private function asyncReqWithRetry(RequestInterface $request, int $retries) { - return $this->httpClient->sendAsyncRequest($request)->then(function($response) use ($request, $retries) { + return $this->httpClient->sendAsyncRequest($request)->then(function ($response) use ($request, $retries) { $status = $response->getStatusCode(); if ($status >= 500 && $status <= 599 && $retries > 0) { - return $this->asyncReqWithRetry($request, $retries-1); + return $this->asyncReqWithRetry($request, $retries - 1); } return $response; }); @@ -164,12 +181,13 @@ private function asyncReqWithRetry($request, $retries) * * @param string $method * @param string $uri - * @param array $payload - * @param array $headers + * @param array $payload + * @param array $headers * * @return array $requestValues + * @throws \Exception */ - public function buildRequestValues($method, $uri, $payload, $headers) + public function buildRequestValues(string $method, string $uri, array $payload, array $headers): array { $method = trim(strtoupper($method)); @@ -186,6 +204,11 @@ public function buildRequestValues($method, $uri, $payload, $headers) // old form-feed workaround now removed $body = json_encode($body); + + if ($body === false) { + throw new \Exception('JSON encoding error: ' . json_last_error_msg()); + } + return [ 'method' => $method, 'url' => $url, @@ -197,26 +220,41 @@ public function buildRequestValues($method, $uri, $payload, $headers) /** * Build RequestInterface from given params. * - * @param array $requestValues - * + * @param string $method + * @param string $url + * @param array $headers + * @param array|string $body * @return RequestInterface */ - public function buildRequestInstance($method, $url, $headers, $body) + public function buildRequestInstance(string $method, string $url, array $headers, $body): RequestInterface { - return $this->getMessageFactory()->createRequest($method, $url, $headers, $body); + $request = $this->getRequestFactory()->createRequest($method, $url); + + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + if (isset($body) && $body !== '' && $body !== []) { + $request = $request->withBody($this->getStreamFactory()->createStream($body)); + } + + return $request; } /** * Build RequestInterface from given params. * - * @param array $requestValues - * + * @param string $method + * @param string $uri + * @param array $payload + * @param array $headers * @return RequestInterface + * @throws \Exception */ - public function buildRequest($method, $uri, $payload, $headers) + public function buildRequest(string $method, string $uri, array $payload, array $headers): RequestInterface { $requestValues = $this->buildRequestValues($method, $uri, $payload, $headers); - return call_user_func_array(array($this, 'buildRequestInstance'), $requestValues); + return call_user_func_array([$this, 'buildRequestInstance'], $requestValues); } /** @@ -226,12 +264,12 @@ public function buildRequest($method, $uri, $payload, $headers) * * @return array $headers - headers for the request */ - public function getHttpHeaders($headers = []) + public function getHttpHeaders(array $headers = []): array { $constantHeaders = [ 'Authorization' => $this->options['key'], 'Content-Type' => 'application/json', - 'User-Agent' => 'php-sparkpost/'.$this->version, + 'User-Agent' => 'php-sparkpost/' . $this->version, ]; foreach ($constantHeaders as $key => $value) { @@ -244,12 +282,12 @@ public function getHttpHeaders($headers = []) /** * Builds the request url from the options and given params. * - * @param string $path - the path in the url to hit - * @param array $params - query parameters to be encoded into the url + * @param string $path - the path in the url to hit + * @param array $params - query parameters to be encoded into the url * * @return string $url - the url to send the desired request to */ - public function getUrl($path, $params = []) + public function getUrl(string $path, array $params = []): string { $options = $this->options; @@ -259,25 +297,31 @@ public function getUrl($path, $params = []) $value = implode(',', $value); } - array_push($paramsArray, $key.'='.$value); + $paramsArray[] = $key . '=' . $value; } $paramsString = implode('&', $paramsArray); - return $options['protocol'].'://'.$options['host'].($options['port'] ? ':'.$options['port'] : '').'/api/'.$options['version'].'/'.$path.($paramsString ? '?'.$paramsString : ''); + return $options['protocol'] . '://' . $options['host'] . ($options['port'] ? ':' . $options['port'] : '') . '/api/' . $options['version'] . '/' . $path . ($paramsString !== '' && $paramsString !== '0' ? '?' . $paramsString : ''); } /** * Sets $httpClient to be used for request. * - * @param HttpClient|HttpAsyncClient $httpClient - the client to be used for request + * @param ClientInterface|HttpAsyncClient $httpClient - the client to be used for request * * @return SparkPost */ - public function setHttpClient($httpClient) + public function setHttpClient($httpClient): self { - if (!($httpClient instanceof HttpAsyncClient || $httpClient instanceof HttpClient)) { - throw new \LogicException(sprintf('Parameter to SparkPost::setHttpClient must be instance of "%s" or "%s"', HttpClient::class, HttpAsyncClient::class)); + if (!$httpClient instanceof ClientInterface && !$httpClient instanceof HttpAsyncClient) { + throw new \LogicException( + sprintf( + 'Parameter to SparkPost::setHttpClient must be instance of "%s" or "%s"', + ClientInterface::class, + HttpAsyncClient::class + ) + ); } $this->httpClient = $httpClient; @@ -288,27 +332,31 @@ public function setHttpClient($httpClient) /** * Sets the options from the param and defaults for the SparkPost object. * - * @param array $options - either an string API key or an array of options + * @param string|array $options - either an string API key or an array of options * * @return SparkPost + * @throws \Exception */ - public function setOptions($options) + public function setOptions($options): self { // if the options map is a string we should assume that its an api key if (is_string($options)) { $options = ['key' => $options]; } + if (!isset($this->options)) { + $this->options = self::$defaultOptions; + } + // Validate API key because its required - if (!isset($this->options['key']) && (!isset($options['key']) || !preg_match('/\S/', $options['key']))) { + $keyToValidate = $options['key'] ?? $this->options['key'] ?? ''; + if (!preg_match('/\S/', $keyToValidate)) { throw new \Exception('You must provide an API key'); } - $this->options = isset($this->options) ? $this->options : self::$defaultOptions; - // set options, overriding defaults foreach ($options as $option => $value) { - if (key_exists($option, $this->options)) { + if (array_key_exists($option, $this->options)) { $this->options[$option] = $value; } } @@ -319,11 +367,11 @@ public function setOptions($options) /** * Returns the given value if debugging, an empty instance otherwise. * - * @param any $param + * @param array $param * - * @return any $param + * @return array|null $param */ - private function ifDebug($param) + private function ifDebug(array $param): ?array { return $this->options['debug'] ? $param : null; } @@ -331,31 +379,55 @@ private function ifDebug($param) /** * Sets up any endpoints to custom classes e.g. $this->transmissions. */ - private function setupEndpoints() + private function setupEndpoints(): void { $this->transmissions = new Transmission($this); } /** - * @return RequestFactory + * @return RequestFactoryInterface */ - private function getMessageFactory() + private function getRequestFactory(): RequestFactoryInterface { - if (!$this->messageFactory) { - $this->messageFactory = MessageFactoryDiscovery::find(); + if (!isset($this->requestFactory)) { + $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); } - return $this->messageFactory; + return $this->requestFactory; + } + + /** + * @return StreamFactoryInterface + */ + private function getStreamFactory(): StreamFactoryInterface + { + if (!isset($this->streamFactory)) { + $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + } + + return $this->streamFactory; + } + + /** + * @param RequestFactoryInterface $requestFactory + * + * @return SparkPost + */ + public function setRequestFactory(RequestFactoryInterface $requestFactory): self + { + $this->requestFactory = $requestFactory; + + return $this; } /** - * @param RequestFactory $messageFactory + * @param StreamFactoryInterface $streamFactory * * @return SparkPost */ - public function setMessageFactory(RequestFactory $messageFactory) + public function setStreamFactory(StreamFactoryInterface $streamFactory): self { - $this->messageFactory = $messageFactory; + $this->streamFactory = $streamFactory; return $this; } diff --git a/lib/SparkPost/SparkPostException.php b/lib/SparkPost/SparkPostException.php index bde2e5e..bbec4cb 100644 --- a/lib/SparkPost/SparkPostException.php +++ b/lib/SparkPost/SparkPostException.php @@ -9,19 +9,20 @@ class SparkPostException extends \Exception /** * Variable to hold json decoded body from http response. */ - private $body = null; + private ?array $body = null; /** * Array with the request values sent. */ - private $request; + private ?array $request; /** * Sets up the custom exception and copies over original exception values. * - * @param Exception $exception - the exception to be wrapped + * @param \Throwable $exception - the exception to be wrapped + * @param null $request */ - public function __construct(\Exception $exception, $request = null) + public function __construct(\Throwable $exception, $request = null) { $this->request = $request; @@ -33,7 +34,7 @@ public function __construct(\Exception $exception, $request = null) $code = $exception->getResponse()->getStatusCode(); } - parent::__construct($message, $code, $exception->getPrevious()); + parent::__construct($message, $code, $exception); } /** @@ -41,7 +42,7 @@ public function __construct(\Exception $exception, $request = null) * * @return array $request */ - public function getRequest() + public function getRequest(): ?array { return $this->request; } @@ -51,7 +52,7 @@ public function getRequest() * * @return array $body - the json decoded body from the http response */ - public function getBody() + public function getBody(): ?array { return $this->body; } diff --git a/lib/SparkPost/SparkPostPromise.php b/lib/SparkPost/SparkPostPromise.php index b7ded0c..6d671bd 100644 --- a/lib/SparkPost/SparkPostPromise.php +++ b/lib/SparkPost/SparkPostPromise.php @@ -9,19 +9,20 @@ class SparkPostPromise implements HttpPromise /** * HttpPromise to be wrapped by SparkPostPromise. */ - private $promise; + private HttpPromise $promise; /** * Array with the request values sent. */ - private $request; + private ?array $request; /** * set the promise to be wrapped. * * @param HttpPromise $promise + * @param array|null $request */ - public function __construct(HttpPromise $promise, $request = null) + public function __construct(HttpPromise $promise, ?array $request = null) { $this->promise = $promise; $this->request = $request; @@ -30,18 +31,19 @@ public function __construct(HttpPromise $promise, $request = null) /** * Hand off the response functions to the original promise and return a custom response or exception. * - * @param callable $onFulfilled - function to be called if the promise is fulfilled - * @param callable $onRejected - function to be called if the promise is rejected + * @param callable|null $onFulfilled - function to be called if the promise is fulfilled + * @param callable|null $onRejected - function to be called if the promise is rejected + * @return HttpPromise */ - public function then(callable $onFulfilled = null, callable $onRejected = null) + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): HttpPromise { $request = $this->request; - return $this->promise->then(function ($response) use ($onFulfilled, $request) { + return $this->promise->then(function ($response) use ($onFulfilled, $request): void { if (isset($onFulfilled)) { $onFulfilled(new SparkPostResponse($response, $request)); } - }, function ($exception) use ($onRejected, $request) { + }, function ($exception) use ($onRejected, $request): void { if (isset($onRejected)) { $onRejected(new SparkPostException($exception, $request)); } @@ -51,9 +53,9 @@ public function then(callable $onFulfilled = null, callable $onRejected = null) /** * Hand back the state. * - * @return $state - returns the state of the promise + * @return string $state - returns the state of the promise */ - public function getState() + public function getState(): string { return $this->promise->getState(); } @@ -67,13 +69,13 @@ public function getState() * * @throws SparkPostException */ - public function wait($unwrap = true) + public function wait($unwrap = true): SparkPostResponse { try { $response = $this->promise->wait($unwrap); return $response ? new SparkPostResponse($response, $this->request) : $response; - } catch (\Exception $exception) { + } catch (\Throwable $exception) { throw new SparkPostException($exception, $this->request); } } diff --git a/lib/SparkPost/SparkPostResponse.php b/lib/SparkPost/SparkPostResponse.php index 402a2b2..a7ba1c3 100644 --- a/lib/SparkPost/SparkPostResponse.php +++ b/lib/SparkPost/SparkPostResponse.php @@ -2,6 +2,7 @@ namespace SparkPost; +use Psr\Http\Message\MessageInterface as MessageInterface; use Psr\Http\Message\ResponseInterface as ResponseInterface; use Psr\Http\Message\StreamInterface as StreamInterface; @@ -10,17 +11,18 @@ class SparkPostResponse implements ResponseInterface /** * ResponseInterface to be wrapped by SparkPostResponse. */ - private $response; + private ResponseInterface $response; /** * Array with the request values sent. */ - private $request; + private ?array $request; /** * set the response to be wrapped. * * @param ResponseInterface $response + * @param null $request */ public function __construct(ResponseInterface $response, $request = null) { @@ -33,90 +35,96 @@ public function __construct(ResponseInterface $response, $request = null) * * @return array $request */ - public function getRequest() + public function getRequest(): ?array { return $this->request; } /** - * Returns the body. - * - * @return array $body - the json decoded body from the http response + * Returns the body as StreamInterface (PSR-7). */ - public function getBody() + public function getBody(): StreamInterface { - $body = $this->response->getBody(); - $body_string = $body->__toString(); + return $this->response->getBody(); + } + /** + * Returns the json decoded body from the http response. + * + * @return array $body + */ + public function getBodyDecoded(): array + { + $body_string = $this->response->getBody()->__toString(); $json = json_decode($body_string, true); - return $json; + return $json ?? []; } /** * pass these down to the response given in the constructor. */ - public function getProtocolVersion() + public function getProtocolVersion(): string { return $this->response->getProtocolVersion(); } - public function withProtocolVersion($version) + public function withProtocolVersion(string $version): MessageInterface { return $this->response->withProtocolVersion($version); } - public function getHeaders() + public function getHeaders(): array { return $this->response->getHeaders(); } - public function hasHeader($name) + public function hasHeader(string $name): bool { return $this->response->hasHeader($name); } - public function getHeader($name) + public function getHeader(string $name): array { return $this->response->getHeader($name); } - public function getHeaderLine($name) + public function getHeaderLine(string $name): string { return $this->response->getHeaderLine($name); } - public function withHeader($name, $value) + public function withHeader(string $name, $value): MessageInterface { return $this->response->withHeader($name, $value); } - public function withAddedHeader($name, $value) + public function withAddedHeader(string $name, $value): MessageInterface { return $this->response->withAddedHeader($name, $value); } - public function withoutHeader($name) + public function withoutHeader(string $name): MessageInterface { return $this->response->withoutHeader($name); } - public function withBody(StreamInterface $body) + public function withBody(StreamInterface $body): MessageInterface { return $this->response->withBody($body); } - public function getStatusCode() + public function getStatusCode(): int { return $this->response->getStatusCode(); } - public function withStatus($code, $reasonPhrase = '') + public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface { return $this->response->withStatus($code, $reasonPhrase); } - public function getReasonPhrase() + public function getReasonPhrase(): string { return $this->response->getReasonPhrase(); } diff --git a/lib/SparkPost/Transmission.php b/lib/SparkPost/Transmission.php index 8efc934..8b1205d 100644 --- a/lib/SparkPost/Transmission.php +++ b/lib/SparkPost/Transmission.php @@ -30,13 +30,13 @@ public function post($payload = [], $headers = []) * * @return array - the modified request body */ - public function formatPayload($payload) + public function formatPayload(array $payload): array { $payload = $this->formatBlindCarbonCopy($payload); //Fixes BCCs into payload $payload = $this->formatCarbonCopy($payload); //Fixes CCs into payload - $payload = $this->formatShorthandRecipients($payload); //Fixes shorthand recipients format + //Fixes shorthand recipients format - return $payload; + return $this->formatShorthandRecipients($payload); } /** @@ -46,9 +46,8 @@ public function formatPayload($payload) * * @return array - the modified request body */ - private function formatBlindCarbonCopy($payload) + private function formatBlindCarbonCopy(array $payload): array { - //If there's a list of BCC recipients, move them into the correct format if (isset($payload['bcc'])) { $payload = $this->addListToRecipients($payload, 'bcc'); @@ -64,16 +63,17 @@ private function formatBlindCarbonCopy($payload) * * @return array - the modified request body */ - private function formatCarbonCopy($payload) + private function formatCarbonCopy(array $payload): array { if (isset($payload['cc'])) { $ccAddresses = []; - for ($i = 0; $i < count($payload['cc']); ++$i) { - array_push($ccAddresses, $this->toAddressString($payload['cc'][$i]['address'])); + $counter = count($payload['cc']); + for ($i = 0; $i < $counter; ++$i) { + $ccAddresses[] = $this->toAddressString($payload['cc'][$i]['address']); } // set up the content headers as either what it was before or an empty array - $payload['content']['headers'] = isset($payload['content']['headers']) ? $payload['content']['headers'] : []; + $payload['content']['headers'] ??= []; // add cc header $payload['content']['headers']['CC'] = implode(',', $ccAddresses); @@ -89,14 +89,16 @@ private function formatCarbonCopy($payload) * @param array $payload - the request body * * @return array - the modified request body + * @throws \Exception */ - private function formatShorthandRecipients($payload) + private function formatShorthandRecipients(array $payload): array { if (isset($payload['content']['from'])) { $payload['content']['from'] = $this->toAddressObject($payload['content']['from']); } + $counter = count($payload['recipients']); - for ($i = 0; $i < count($payload['recipients']); ++$i) { + for ($i = 0; $i < $counter; ++$i) { $payload['recipients'][$i]['address'] = $this->toAddressObject($payload['recipients'][$i]['address']); } @@ -106,12 +108,13 @@ private function formatShorthandRecipients($payload) /** * Loops through the given listName in the payload and adds all the recipients to the recipients list after removing their names. * - * @param array $payload - the request body - * @param array $listName - the name of the array in the payload to be moved to the recipients list + * @param array $payload - the request body + * @param string $listName - the name of the array in the payload to be moved to the recipients list * * @return array - the modified request body + * @throws \Exception */ - private function addListToRecipients($payload, $listName) + private function addListToRecipients(array $payload, string $listName): array { $originalAddress = $this->toAddressString($payload['recipients'][0]['address']); foreach ($payload[$listName] as $recipient) { @@ -123,7 +126,7 @@ private function addListToRecipients($payload, $listName) unset($recipient['address']['name']); } - array_push($payload['recipients'], $recipient); + $payload['recipients'][] = $recipient; } //Delete the original object from the payload. @@ -138,8 +141,9 @@ private function addListToRecipients($payload, $listName) * @param $address - the shorthand form of an email address "Name " * * @return array - the longhand form of an email address [ "name" => "John", "email" => "john@exmmple.com" ] + * @throws \Exception */ - private function toAddressObject($address) + private function toAddressObject($address): array { $formatted = $address; if (is_string($formatted)) { @@ -152,7 +156,7 @@ private function toAddressObject($address) $formatted['name'] = $matches[1]; $formatted['email'] = $matches[2]; } else { - throw new \Exception('Invalid address format: '.$address); + throw new \Exception('Invalid address format: ' . $address); } } @@ -162,18 +166,13 @@ private function toAddressObject($address) /** * Takes the longhand form of an email address and converts it to the shorthand form. * - * @param $address - the longhand form of an email address [ "name" => "John", "email" => "john@exmmple.com" ] - * @param string - the shorthand form of an email address "Name " + * @param string|array $address - the longhand form of an email address [ "name" => "John", "email" => "john@exmmple.com" ] */ - private function toAddressString($address) + private function toAddressString($address): string { // convert object to string if (!is_string($address)) { - if (isset($address['name'])) { - $address = '"'.$address['name'].'" <'.$address['email'].'>'; - } else { - $address = $address['email']; - } + $address = isset($address['name']) ? '"' . $address['name'] . '" <' . $address['email'] . '>' : $address['email']; } return $address; @@ -183,9 +182,9 @@ private function toAddressString($address) * Checks if a string is an email. * * @param string $email - a string that might be an email address - * @param bool - true if the given string is an email + * @return bool - true if the given string is an email */ - private function isEmail($email) + private function isEmail(string $email): bool { if (filter_var($email, FILTER_VALIDATE_EMAIL)) { return true; diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..55b2ebe --- /dev/null +++ b/rector.php @@ -0,0 +1,26 @@ +paths([ + __DIR__ . '/examples', + __DIR__ . '/lib', + __DIR__ . '/test', + ]); + + // register a single rule + $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_74, + SetList::TYPE_DECLARATION, + SetList::CODE_QUALITY, + ]); +}; diff --git a/test/unit/IssueReproductionTest.php b/test/unit/IssueReproductionTest.php new file mode 100644 index 0000000..48533c9 --- /dev/null +++ b/test/unit/IssueReproductionTest.php @@ -0,0 +1,28 @@ + 'test-key']); + + // Invalid UTF-8 sequence to make json_encode return false + $payload = ['invalid' => "\xB1\x31"]; + + // We expect an Exception because json_encode fails + try { + $sparkpost->buildRequest('POST', 'test', $payload, []); + $this->fail('Expected \Exception was not thrown'); + } catch (\Exception $e) { + $this->assertStringContainsString('JSON encoding error', $e->getMessage()); + } + } +} diff --git a/test/unit/SparkPostResponseTest.php b/test/unit/SparkPostResponseTest.php index 90f16be..03485d6 100644 --- a/test/unit/SparkPostResponseTest.php +++ b/test/unit/SparkPostResponseTest.php @@ -3,46 +3,77 @@ namespace SparkPost\Test; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use SparkPost\SparkPostResponse; use Mockery; class SparkPostResponseTest extends TestCase { - /** @var Mockery\MockInterface|\Psr\Http\Message\ResponseInterface */ + /** @var Mockery\MockInterface|ResponseInterface */ private $responseMock; /** @var string */ - private $returnValue; + private string $returnValue; public function setUp(): void { $this->returnValue = 'some_value_to_return'; - $this->responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $this->responseMock = Mockery::mock(ResponseInterface::class); } - public function testGetProtocolVersion() + /** + * Test that getProtocolVersion returns the protocol version from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getProtocolVersion, and verifies the wrapper returns the same value. + */ + public function testGetProtocolVersion(): void { $this->responseMock->shouldReceive('getProtocolVersion')->andReturn($this->returnValue); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->getProtocolVersion(), $sparkpostResponse->getProtocolVersion()); } - public function testWithProtocolVersion() + /** + * Test that withProtocolVersion returns a new instance with the specified protocol version. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withProtocolVersion, and verifies the returned object matches the mock result. + */ + public function testWithProtocolVersion(): void { $param = 'protocol version'; + $messageMock = Mockery::mock(MessageInterface::class); - $this->responseMock->shouldReceive('withProtocolVersion')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withProtocolVersion')->andReturn($messageMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); - $this->assertEquals($this->responseMock->withProtocolVersion($param), $sparkpostResponse->withProtocolVersion($param)); + $this->assertEquals( + $this->responseMock->withProtocolVersion($param), + $sparkpostResponse->withProtocolVersion($param) + ); } - public function testGetHeaders() + /** + * Test that getHeaders returns the headers from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getHeaders, and verifies the wrapper returns the same array. + */ + public function testGetHeaders(): void { - $this->responseMock->shouldReceive('getHeaders')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('getHeaders')->andReturn([]); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->getHeaders(), $sparkpostResponse->getHeaders()); } - public function testHasHeader() + /** + * Test that hasHeader returns whether a header exists in the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for hasHeader, and verifies the wrapper returns the expected boolean/value. + */ + public function testHasHeader(): void { $param = 'header'; @@ -51,16 +82,28 @@ public function testHasHeader() $this->assertEquals($this->responseMock->hasHeader($param), $sparkpostResponse->hasHeader($param)); } - public function testGetHeader() + /** + * Test that getHeader returns the specified header from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getHeader, and verifies the wrapper returns the same array. + */ + public function testGetHeader(): void { $param = 'header'; - $this->responseMock->shouldReceive('getHeader')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('getHeader')->andReturn([]); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->getHeader($param), $sparkpostResponse->getHeader($param)); } - public function testGetHeaderLine() + /** + * Test that getHeaderLine returns the specified header line from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getHeaderLine, and verifies the wrapper returns the same string. + */ + public function testGetHeaderLine(): void { $param = 'header'; @@ -69,36 +112,69 @@ public function testGetHeaderLine() $this->assertEquals($this->responseMock->getHeaderLine($param), $sparkpostResponse->getHeaderLine($param)); } - public function testWithHeader() + /** + * Test that withHeader returns a new instance with the specified header. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withHeader, and verifies the returned object matches the mock result. + */ + public function testWithHeader(): void { $param = 'header'; $param2 = 'value'; + $messageMock = Mockery::mock(MessageInterface::class); - $this->responseMock->shouldReceive('withHeader')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withHeader')->andReturn($messageMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); - $this->assertEquals($this->responseMock->withHeader($param, $param2), $sparkpostResponse->withHeader($param, $param2)); + $this->assertEquals( + $this->responseMock->withHeader($param, $param2), + $sparkpostResponse->withHeader($param, $param2) + ); } - public function testWithAddedHeader() + /** + * Test that withAddedHeader returns a new instance with the specified header added. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withAddedHeader, and verifies the returned object matches the mock result. + */ + public function testWithAddedHeader(): void { $param = 'header'; $param2 = 'value'; + $messageMock = Mockery::mock(MessageInterface::class); - $this->responseMock->shouldReceive('withAddedHeader')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withAddedHeader')->andReturn($messageMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); - $this->assertEquals($this->responseMock->withAddedHeader($param, $param2), $sparkpostResponse->withAddedHeader($param, $param2)); + $this->assertEquals( + $this->responseMock->withAddedHeader($param, $param2), + $sparkpostResponse->withAddedHeader($param, $param2) + ); } - public function testWithoutHeader() + /** + * Test that withoutHeader returns a new instance without the specified header. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withoutHeader, and verifies the returned object matches the mock result. + */ + public function testWithoutHeader(): void { $param = 'header'; + $messageMock = Mockery::mock(MessageInterface::class); - $this->responseMock->shouldReceive('withoutHeader')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withoutHeader')->andReturn($messageMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->withoutHeader($param), $sparkpostResponse->withoutHeader($param)); } - public function testGetRequest() + /** + * Test that getRequest returns the request data passed during construction. + * + * Why: Ensures that the SparkPostResponse correctly stores and retrieves the request metadata used for the API call. + * How: Initializes SparkPostResponse with a request array and verifies getRequest() returns that same array. + */ + public function testGetRequest(): void { $request = ['some' => 'request']; $this->responseMock->shouldReceive('getRequest')->andReturn($request); @@ -106,32 +182,57 @@ public function testGetRequest() $this->assertEquals($sparkpostResponse->getRequest(), $request); } - public function testWithBody() + /** + * Test that withBody returns a new instance with the specified body. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withBody, and verifies the returned object matches the mock result. + */ + public function testWithBody(): void { - $param = Mockery::mock('Psr\Http\Message\StreamInterface'); + $param = Mockery::mock(StreamInterface::class); + $messageMock = Mockery::mock(MessageInterface::class); - $this->responseMock->shouldReceive('withBody')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withBody')->andReturn($messageMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->withBody($param), $sparkpostResponse->withBody($param)); } - public function testGetStatusCode() + /** + * Test that getStatusCode returns the status code from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getStatusCode, and verifies the wrapper returns the same integer. + */ + public function testGetStatusCode(): void { - $this->responseMock->shouldReceive('getStatusCode')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('getStatusCode')->andReturn(200); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->getStatusCode(), $sparkpostResponse->getStatusCode()); } - public function testWithStatus() + /** + * Test that withStatus returns a new instance with the specified status code. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for withStatus, and verifies the returned object matches the mock result. + */ + public function testWithStatus(): void { - $param = 'status'; + $param = 200; - $this->responseMock->shouldReceive('withStatus')->andReturn($this->returnValue); + $this->responseMock->shouldReceive('withStatus')->andReturn($this->responseMock); $sparkpostResponse = new SparkPostResponse($this->responseMock); $this->assertEquals($this->responseMock->withStatus($param), $sparkpostResponse->withStatus($param)); } - public function testGetReasonPhrase() + /** + * Test that getReasonPhrase returns the reason phrase from the wrapped response. + * + * Why: Ensures that the SparkPostResponse correctly delegates the call to the underlying PSR-7 response object. + * How: Mocks ResponseInterface, sets expectation for getReasonPhrase, and verifies the wrapper returns the same string. + */ + public function testGetReasonPhrase(): void { $this->responseMock->shouldReceive('getReasonPhrase')->andReturn($this->returnValue); $sparkpostResponse = new SparkPostResponse($this->responseMock); diff --git a/test/unit/SparkPostTest.php b/test/unit/SparkPostTest.php index d707ced..a07bc1a 100644 --- a/test/unit/SparkPostTest.php +++ b/test/unit/SparkPostTest.php @@ -2,33 +2,43 @@ namespace SparkPost\Test; +use Http\Client\Exception; +use Http\Client\Exception\HttpException; use Http\Client\HttpAsyncClient; -use Http\Client\HttpClient; -use Http\Message\MessageFactory; +use Http\Promise\Promise; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\RequestInterface; use Nyholm\NSA; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use SparkPost\SparkPost; +use SparkPost\SparkPostException; use SparkPost\SparkPostPromise; use GuzzleHttp\Promise\FulfilledPromise as GuzzleFulfilledPromise; use GuzzleHttp\Promise\RejectedPromise as GuzzleRejectedPromise; -use Http\Adapter\Guzzle6\Promise as GuzzleAdapterPromise; +use Http\Adapter\Guzzle7\Promise as GuzzleAdapterPromise; use Mockery; +use SparkPost\SparkPostResponse; class SparkPostTest extends TestCase { + public array $badResponseBody; + public $badResponseMock; private $clientMock; /** @var SparkPost */ - private $resource; + private SparkPost $resource; private $exceptionMock; - private $exceptionBody; + private array $exceptionBody; private $responseMock; - private $responseBody; + private array $responseBody; private $promiseMock; - private $postTransmissionPayload = [ + private array $postTransmissionPayload = [ 'content' => [ 'from' => ['name' => 'Sparkpost Team', 'email' => 'postmaster@sendmailfor.me'], 'subject' => 'First Mailing From PHP', @@ -40,43 +50,48 @@ class SparkPostTest extends TestCase ], ]; - private $getTransmissionPayload = [ + private array $getTransmissionPayload = [ 'campaign_id' => 'thanksgiving', ]; + /** + * @throws \Exception + */ public function setUp(): void { // response mock up - $responseBodyMock = Mockery::mock(); + $responseBodyMock = Mockery::mock(StreamInterface::class); $this->responseBody = ['results' => 'yay']; - $this->responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $this->responseMock = Mockery::mock(ResponseInterface::class); $this->responseMock->shouldReceive('getStatusCode')->andReturn(200); $this->responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); $responseBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->responseBody)); - $errorBodyMock = Mockery::mock(); + $errorBodyMock = Mockery::mock(StreamInterface::class); $this->badResponseBody = ['errors' => []]; - $this->badResponseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $this->badResponseMock = Mockery::mock(ResponseInterface::class); $this->badResponseMock->shouldReceive('getStatusCode')->andReturn(503); $this->badResponseMock->shouldReceive('getBody')->andReturn($errorBodyMock); $errorBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->badResponseBody)); // exception mock up - $exceptionResponseMock = Mockery::mock(); + $exceptionResponseMock = Mockery::mock(ResponseInterface::class); + $exceptionResponseBodyMock = Mockery::mock(StreamInterface::class); $this->exceptionBody = ['results' => 'failed']; - $this->exceptionMock = Mockery::mock('Http\Client\Exception\HttpException'); + $this->exceptionMock = Mockery::mock(HttpException::class); $this->exceptionMock->shouldReceive('getResponse')->andReturn($exceptionResponseMock); $exceptionResponseMock->shouldReceive('getStatusCode')->andReturn(500); - $exceptionResponseMock->shouldReceive('getBody->__toString')->andReturn(json_encode($this->exceptionBody)); + $exceptionResponseMock->shouldReceive('getBody')->andReturn($exceptionResponseBodyMock); + $exceptionResponseBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->exceptionBody)); // promise mock up - $this->promiseMock = Mockery::mock('Http\Promise\Promise'); + $this->promiseMock = Mockery::mock(Promise::class); //setup mock for the adapter - $this->clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client'); - $this->clientMock->shouldReceive('sendAsyncRequest')-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($this->promiseMock); + $this->clientMock = Mockery::mock(ClientInterface::class, HttpAsyncClient::class); + $this->clientMock->shouldReceive('sendAsyncRequest') + ->with(Mockery::type(RequestInterface::class)) + ->andReturn($this->promiseMock); $this->resource = new SparkPost($this->clientMock, ['key' => 'SPARKPOST_API_KEY']); } @@ -86,33 +101,77 @@ public function tearDown(): void Mockery::close(); } - public function testRequestSync() + /** + * Test that request() returns a SparkPostResponse when async option is false. + * + * Why: Ensures the SparkPost client correctly handles synchronous request mode. + * How: Sets 'async' to false, mocks the client's sendRequest method, and asserts the returned object is a SparkPostResponse. + * + * @throws SparkPostException + * @throws \Exception + */ + public function testRequestSync(): void { $this->resource->setOptions(['async' => false]); $this->clientMock->shouldReceive('sendRequest')->andReturn($this->responseMock); - $this->assertInstanceOf('SparkPost\SparkPostResponse', $this->resource->request('POST', 'transmissions', $this->postTransmissionPayload)); + $this->assertInstanceOf( + SparkPostResponse::class, + $this->resource->request('POST', 'transmissions', $this->postTransmissionPayload) + ); } - public function testRequestAsync() + /** + * Test that request() returns a SparkPostPromise when async option is true. + * + * Why: Ensures the SparkPost client correctly handles asynchronous request mode. + * How: Sets 'async' to true, mocks the client's sendAsyncRequest method, and asserts the returned object is a SparkPostPromise. + * + * @throws SparkPostException + * @throws \Exception + */ + public function testRequestAsync(): void { - $promiseMock = Mockery::mock('Http\Promise\Promise'); + $promiseMock = Mockery::mock(Promise::class); $this->resource->setOptions(['async' => true]); $this->clientMock->shouldReceive('sendAsyncRequest')->andReturn($promiseMock); - $this->assertInstanceOf('SparkPost\SparkPostPromise', $this->resource->request('GET', 'transmissions', $this->getTransmissionPayload)); + $this->assertInstanceOf( + SparkPostPromise::class, + $this->resource->request('GET', 'transmissions', $this->getTransmissionPayload) + ); } - public function testDebugOptionWhenFalse() { + /** + * Test that the debug option, when false, does not include request data in the response. + * + * Why: Verifies that sensitive or large request data is not attached to the response object by default. + * How: Sets 'debug' to false, executes a request, and asserts that getRequest() on the resulting response returns null. + * + * @throws SparkPostException + * @throws \Exception + */ + public function testDebugOptionWhenFalse(): void + { $this->resource->setOptions(['async' => false, 'debug' => false]); $this->clientMock->shouldReceive('sendRequest')->andReturn($this->responseMock); $response = $this->resource->request('POST', 'transmissions', $this->postTransmissionPayload); - $this->assertEquals($response->getRequest(), null); + $this->assertEquals(null, $response->getRequest()); } - public function testDebugOptionWhenTrue() { + /** + * Test that the debug option, when true, includes request data in successful and failed responses. + * + * Why: Ensures developers can access original request parameters from response/exception objects for debugging. + * How: Sets 'debug' to true, executes requests (one success, one failure), and verifies that request data is correctly attached. + * + * @throws SparkPostException + * @throws \Exception + */ + public function testDebugOptionWhenTrue(): void + { // setup $this->resource->setOptions(['async' => false, 'debug' => true]); @@ -126,59 +185,92 @@ public function testDebugOptionWhenTrue() { try { $response = $this->resource->request('POST', 'transmissions', $this->postTransmissionPayload); - } - catch (\Exception $e) { + } catch (\Exception $e) { $this->assertEquals(json_decode($e->getRequest()['body'], true), $this->postTransmissionPayload); } } - public function testSuccessfulSyncRequest() + /** + * Test a successful synchronous request. + * + * Why: Verifies that syncRequest() correctly processes a successful API call. + * How: Mocks a successful sendRequest call and asserts the SparkPostResponse contains the expected body and status code. + * + * @throws Exception + * @throws SparkPostException + */ + public function testSuccessfulSyncRequest(): void { $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($this->responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($this->responseMock); $response = $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload); - $this->assertEquals($this->responseBody, $response->getBody()); + $this->assertEquals($this->responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testUnsuccessfulSyncRequest() + /** + * Test an unsuccessful synchronous request. + * + * Why: Ensures that HTTP client exceptions are properly wrapped and re-thrown as SparkPostExceptions. + * How: Mocks a client exception during sendRequest and verifies that a SparkPostException is caught with correct error details. + */ + public function testUnsuccessfulSyncRequest(): void { $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andThrow($this->exceptionMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andThrow($this->exceptionMock); try { $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload); - } catch (\Exception $e) { + } catch (\Exception|Exception $e) { $this->assertEquals($this->exceptionBody, $e->getBody()); $this->assertEquals(500, $e->getCode()); } } - public function testSuccessfulSyncRequestWithRetries() + /** + * Test synchronous request with automatic retries on 5xx errors. + * + * Why: Verifies that the client correctly retries transient server errors. + * How: Mocks the client to return multiple 503 responses followed by a 200, and verifies the final success. + * + * @throws Exception + * @throws SparkPostException + * @throws \Exception + */ + public function testSuccessfulSyncRequestWithRetries(): void { $this->clientMock->shouldReceive('sendRequest')-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($this->badResponseMock, $this->badResponseMock, $this->responseMock); + with(Mockery::type(RequestInterface::class))-> + andReturn($this->badResponseMock, $this->badResponseMock, $this->responseMock); $this->resource->setOptions(['retries' => 2]); $response = $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload); - $this->assertEquals($this->responseBody, $response->getBody()); + $this->assertEquals($this->responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testUnsuccessfulSyncRequestWithRetries() + /** + * Test that synchronous retries are exhausted after the specified number of attempts. + * + * Why: Ensures the client doesn't retry indefinitely and eventually reports the failure. + * How: Mocks a persistent client exception and verifies that it is thrown after the configured retry limit is reached. + * + * @throws Exception + * @throws \Exception + */ + public function testUnsuccessfulSyncRequestWithRetries(): void { $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andThrow($this->exceptionMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andThrow($this->exceptionMock); $this->resource->setOptions(['retries' => 2]); try { @@ -189,18 +281,35 @@ public function testUnsuccessfulSyncRequestWithRetries() } } - public function testSuccessfulAsyncRequestWithWait() + /** + * Test a successful asynchronous request followed by wait(). + * + * Why: Verifies the basic async request-response flow using promise wait(). + * How: Mocks an async request that returns a promise, and asserts that wait() on the returned SparkPostPromise yield correct data. + * + * @throws SparkPostException + * @throws \Exception + */ + public function testSuccessfulAsyncRequestWithWait(): void { $this->promiseMock->shouldReceive('wait')->andReturn($this->responseMock); $promise = $this->resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload); $response = $promise->wait(); - $this->assertEquals($this->responseBody, $response->getBody()); + $this->assertEquals($this->responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testUnsuccessfulAsyncRequestWithWait() + /** + * Test an unsuccessful asynchronous request followed by wait(). + * + * Why: Ensures that async failures are correctly reported when wait() is called. + * How: Mocks an async request that returns a rejected promise and verifies wait() throws a SparkPostException. + * + * @throws \Exception + */ + public function testUnsuccessfulAsyncRequestWithWait(): void { $this->promiseMock->shouldReceive('wait')->andThrow($this->exceptionMock); @@ -214,7 +323,15 @@ public function testUnsuccessfulAsyncRequestWithWait() } } - public function testSuccessfulAsyncRequestWithThen() + /** + * Test a successful asynchronous request using then() callbacks. + * + * Why: Verifies that the onFulfilled callback is correctly triggered with a SparkPostResponse. + * How: Uses a Guzzle fulfilled promise and verifies the then() callback receives the expected status and body. + * + * @throws \Throwable + */ + public function testSuccessfulAsyncRequestWithThen(): void { $guzzlePromise = new GuzzleFulfilledPromise($this->responseMock); $result = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []); @@ -222,13 +339,21 @@ public function testSuccessfulAsyncRequestWithThen() $promise = new SparkPostPromise(new GuzzleAdapterPromise($guzzlePromise, $result)); $responseBody = $this->responseBody; - $promise->then(function ($response) use ($responseBody) { + $promise->then(function ($response) use ($responseBody): void { $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals($responseBody, $response->getBody()); - }, null)->wait(); + $this->assertEquals($responseBody, $response->getBodyDecoded()); + })->wait(); } - public function testUnsuccessfulAsyncRequestWithThen() + /** + * Test an unsuccessful asynchronous request using then() callbacks. + * + * Why: Verifies that the onRejected callback is correctly triggered with a SparkPostException. + * How: Uses a Guzzle rejected promise and verifies the then() callback receives the expected error code and body. + * + * @throws \Throwable + */ + public function testUnsuccessfulAsyncRequestWithThen(): void { $guzzlePromise = new GuzzleRejectedPromise($this->exceptionMock); $result = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []); @@ -236,19 +361,27 @@ public function testUnsuccessfulAsyncRequestWithThen() $promise = new SparkPostPromise(new GuzzleAdapterPromise($guzzlePromise, $result)); $exceptionBody = $this->exceptionBody; - $promise->then(null, function ($exception) use ($exceptionBody) { + $promise->then(null, function ($exception) use ($exceptionBody): void { $this->assertEquals(500, $exception->getCode()); $this->assertEquals($exceptionBody, $exception->getBody()); })->wait(); } - public function testSuccessfulAsyncRequestWithRetries() + /** + * Test asynchronous request with automatic retries on 5xx errors. + * + * Why: Verifies that the async promise chain correctly handles transient errors via retries. + * How: Mocks multiple 503 responses in the async client and verifies the final successful response is delivered to the then() callback. + * + * @throws \Throwable + */ + public function testSuccessfulAsyncRequestWithRetries(): void { $testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []); - $clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client'); - $clientMock->shouldReceive('sendAsyncRequest')-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn( + $clientMock = Mockery::mock(ClientInterface::class, HttpAsyncClient::class); + $clientMock->shouldReceive('sendAsyncRequest') + ->with(Mockery::type(RequestInterface::class)) + ->andReturn( new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq), new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq), new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->responseMock), $testReq) @@ -258,31 +391,47 @@ public function testSuccessfulAsyncRequestWithRetries() $resource->setOptions(['async' => true, 'retries' => 2]); $promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload); - $promise->then(function($resp) { + $promise->then(function ($resp): void { $this->assertEquals(200, $resp->getStatusCode()); })->wait(); } - public function testUnsuccessfulAsyncRequestWithRetries() + /** + * Test that asynchronous retries are exhausted after the specified number of attempts. + * + * Why: Ensures the async chain eventually fails if the server is persistently broken. + * How: Mocks a rejected promise and verifies the final error is propagated to the then() rejection callback. + * + * @throws \Throwable + */ + public function testUnsuccessfulAsyncRequestWithRetries(): void { $testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []); $rejectedPromise = new GuzzleRejectedPromise($this->exceptionMock); - $clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client'); - $clientMock->shouldReceive('sendAsyncRequest')-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn(new GuzzleAdapterPromise($rejectedPromise, $testReq)); + $clientMock = Mockery::mock(ClientInterface::class, HttpAsyncClient::class); + $clientMock->shouldReceive('sendAsyncRequest') + ->with(Mockery::type(RequestInterface::class)) + ->andReturn(new GuzzleAdapterPromise($rejectedPromise, $testReq)); $resource = new SparkPost($clientMock, ['key' => 'SPARKPOST_API_KEY']); $resource->setOptions(['async' => true, 'retries' => 2]); $promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload); - $promise->then(null, function($exception) { + $promise->then(null, function ($exception): void { $this->assertEquals(500, $exception->getCode()); $this->assertEquals($this->exceptionBody, $exception->getBody()); })->wait(); } - public function testPromise() + /** + * Test that the SparkPostPromise correctly reports its state. + * + * Why: Verifies that state checks are correctly delegated to the underlying promise. + * How: Mocks the underlying promise's getState method and asserts the SparkPostPromise wrapper returns the same states. + * + * @throws \Exception + */ + public function testPromise(): void { $promise = $this->resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload); @@ -293,16 +442,28 @@ public function testPromise() $this->assertEquals($this->promiseMock->getState(), $promise->getState()); } - public function testUnsupportedAsyncRequest() + /** + * Test that an exception is thrown when attempting an async request with a non-async client. + * + * Why: Prevents runtime errors by validating client capabilities early. + * How: Sets a synchronous-only mock client and verifies that calling asyncRequest throws an exception. + */ + public function testUnsupportedAsyncRequest(): void { $this->expectException(\Exception::class); - $this->resource->setHttpClient(Mockery::mock('Http\Client\HttpClient')); + $this->resource->setHttpClient(Mockery::mock(ClientInterface::class)); $this->resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload); } - public function testGetHttpHeaders() + /** + * Test that HTTP headers are correctly constructed, including defaults and custom headers. + * + * Why: Ensures the API receives mandatory headers (Auth, Content-Type, User-Agent) correctly. + * How: Calls getHttpHeaders with a custom header and asserts all expected headers (default and custom) are present and correct. + */ + public function testGetHttpHeaders(): void { $headers = $this->resource->getHttpHeaders([ 'Custom-Header' => 'testing', @@ -313,45 +474,83 @@ public function testGetHttpHeaders() $this->assertEquals('SPARKPOST_API_KEY', $headers['Authorization']); $this->assertEquals('application/json', $headers['Content-Type']); $this->assertEquals('testing', $headers['Custom-Header']); - $this->assertEquals('php-sparkpost/'.$version, $headers['User-Agent']); + $this->assertEquals('php-sparkpost/' . $version, $headers['User-Agent']); } - public function testGetUrl() + /** + * Test that the API URL is correctly constructed with path and query parameters. + * + * Why: Ensures requests are sent to the correct API endpoint with properly formatted query strings. + * How: Calls getUrl with a path and array of parameters, and asserts the generated URL matches the expected format. + */ + public function testGetUrl(): void { $url = 'https://api.sparkpost.com:443/api/v1/transmissions?key=value 1,value 2,value 3'; $testUrl = $this->resource->getUrl('transmissions', ['key' => ['value 1', 'value 2', 'value 3']]); $this->assertEquals($url, $testUrl); } - public function testSetHttpClient() + /** + * Test that a synchronous HTTP client can be set. + * + * Why: Ensures flexibility in choosing HTTP client implementations. + * How: Sets a mock ClientInterface and uses reflection to verify the private property was updated. + */ + public function testSetHttpClient(): void { - $mock = Mockery::mock(HttpClient::class); + $mock = Mockery::mock(ClientInterface::class); $this->resource->setHttpClient($mock); $this->assertEquals($mock, NSA::getProperty($this->resource, 'httpClient')); } - public function testSetHttpAsyncClient() + /** + * Test that an asynchronous HTTP client can be set. + * + * Why: Ensures flexibility in choosing async-capable HTTP client implementations. + * How: Sets a mock HttpAsyncClient and uses reflection to verify the private property was updated. + */ + public function testSetHttpAsyncClient(): void { $mock = Mockery::mock(HttpAsyncClient::class); $this->resource->setHttpClient($mock); $this->assertEquals($mock, NSA::getProperty($this->resource, 'httpClient')); } - public function testSetHttpClientException() + /** + * Test that an exception is thrown when setting an invalid HTTP client. + * + * Why: Ensures type safety and prevents runtime errors from incompatible client objects. + * How: Attempts to set an invalid object (stdClass) as the HTTP client and asserts an exception is thrown. + */ + public function testSetHttpClientException(): void { $this->expectException(\Exception::class); $this->resource->setHttpClient(new \stdClass()); } - public function testSetOptionsStringKey() + /** + * Test that options can be initialized using just a string as the API key. + * + * Why: Provides a convenient shorthand for common client initialization. + * How: Passes a string to setOptions and verifies the 'key' option is correctly set while others remain at defaults. + * + * @throws \Exception + */ + public function testSetOptionsStringKey(): void { $this->resource->setOptions('SPARKPOST_API_KEY'); $options = NSA::getProperty($this->resource, 'options'); $this->assertEquals('SPARKPOST_API_KEY', $options['key']); } - public function testSetBadOptions() + /** + * Test that an exception is thrown if no API key is provided in the options. + * + * Why: Ensures the client cannot be misconfigured without a mandatory API key. + * How: Attempts to set options without a 'key' and asserts that an exception is thrown. + */ + public function testSetBadOptions(): void { $this->expectException(\Exception::class); @@ -359,11 +558,17 @@ public function testSetBadOptions() $this->resource->setOptions(['not' => 'SPARKPOST_API_KEY']); } - public function testSetMessageFactory() + /** + * Test that a custom PSR-17 Request Factory can be set. + * + * Why: Allows developers to use their preferred PSR-17 implementation. + * How: Sets a mock RequestFactoryInterface and verifies it is retrieved by the internal factory getter. + */ + public function testSetRequestFactory(): void { - $messageFactory = Mockery::mock(MessageFactory::class); - $this->resource->setMessageFactory($messageFactory); + $messageFactory = Mockery::mock(RequestFactoryInterface::class); + $this->resource->setRequestFactory($messageFactory); - $this->assertEquals($messageFactory, NSA::invokeMethod($this->resource, 'getMessageFactory')); + $this->assertEquals($messageFactory, NSA::invokeMethod($this->resource, 'getRequestFactory')); } } diff --git a/test/unit/TransmissionTest.php b/test/unit/TransmissionTest.php index 09bd1cb..4a77f51 100644 --- a/test/unit/TransmissionTest.php +++ b/test/unit/TransmissionTest.php @@ -3,16 +3,22 @@ namespace SparkPost\Test; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use SparkPost\SparkPost; use Mockery; +use Psr\Http\Message\RequestInterface; +use Http\Client\HttpAsyncClient; +use Psr\Http\Client\ClientInterface; +use SparkPost\SparkPostException; class TransmissionTest extends TestCase { private $clientMock; /** @var SparkPost */ - private $resource; + private SparkPost $resource; - private $postTransmissionPayload = [ + private array $postTransmissionPayload = [ 'content' => [ 'from' => ['name' => 'Sparkpost Team', 'email' => 'postmaster@sendmailfor.me'], 'subject' => 'First Mailing From PHP', @@ -41,7 +47,7 @@ class TransmissionTest extends TestCase ]; - private $getTransmissionPayload = [ + private array $getTransmissionPayload = [ 'campaign_id' => 'thanksgiving', ]; @@ -50,12 +56,13 @@ class TransmissionTest extends TestCase * * @before * + * @throws \Exception * @see PHPUnit_Framework_TestCase::setUp() */ public function setUp(): void { //setup mock for the adapter - $this->clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client'); + $this->clientMock = Mockery::mock(ClientInterface::class); $this->resource = new SparkPost($this->clientMock, ['key' => 'SPARKPOST_API_KEY', 'async' => false]); } @@ -65,7 +72,13 @@ public function tearDown(): void Mockery::close(); } - public function testInvalidEmailFormat() + /** + * Test that an invalid email format in recipients throws an exception. + * + * Why: Verifies that the Transmission resource validates email addresses before sending the request to the API. + * How: Adds an invalid email to the payload and asserts that calling post() throws an Exception. + */ + public function testInvalidEmailFormat(): void { $this->expectException(\Exception::class); @@ -76,17 +89,25 @@ public function testInvalidEmailFormat() $response = $this->resource->transmissions->post($this->postTransmissionPayload); } - public function testGet() + /** + * Test a GET request to the transmissions endpoint. + * + * Why: Verifies that the Transmission resource correctly delegates GET requests to the SparkPost client. + * How: Mocks a successful GET request and verifies the response matches the expected mock data. + * + * @throws SparkPostException + */ + public function testGet(): void { - $responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); - $responseBodyMock = Mockery::mock(); + $responseMock = Mockery::mock(ResponseInterface::class); + $responseBodyMock = Mockery::mock(StreamInterface::class); $responseBody = ['results' => 'yay']; $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($responseMock); $responseMock->shouldReceive('getStatusCode')->andReturn(200); $responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); @@ -94,21 +115,29 @@ public function testGet() $response = $this->resource->transmissions->get($this->getTransmissionPayload); - $this->assertEquals($responseBody, $response->getBody()); + $this->assertEquals($responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testPut() + /** + * Test a PUT request to the transmissions endpoint. + * + * Why: Verifies that the Transmission resource correctly delegates PUT requests to the SparkPost client. + * How: Mocks a successful PUT request and verifies the response matches the expected mock data. + * + * @throws SparkPostException + */ + public function testPut(): void { - $responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); - $responseBodyMock = Mockery::mock(); + $responseMock = Mockery::mock(ResponseInterface::class); + $responseBodyMock = Mockery::mock(StreamInterface::class); $responseBody = ['results' => 'yay']; $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($responseMock); $responseMock->shouldReceive('getStatusCode')->andReturn(200); $responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); @@ -116,21 +145,27 @@ public function testPut() $response = $this->resource->transmissions->put($this->getTransmissionPayload); - $this->assertEquals($responseBody, $response->getBody()); + $this->assertEquals($responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testPost() + /** + * Test a POST request to the transmissions endpoint. + * + * Why: Verifies that the Transmission resource correctly formats payloads and sends POST requests. + * How: Mocks a successful POST request and verifies the response matches the expected mock data. + */ + public function testPost(): void { - $responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); - $responseBodyMock = Mockery::mock(); + $responseMock = Mockery::mock(ResponseInterface::class); + $responseBodyMock = Mockery::mock(StreamInterface::class); $responseBody = ['results' => 'yay']; $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($responseMock); $responseMock->shouldReceive('getStatusCode')->andReturn(200); $responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); @@ -138,46 +173,60 @@ public function testPost() $response = $this->resource->transmissions->post($this->postTransmissionPayload); - $this->assertEquals($responseBody, $response->getBody()); + $this->assertEquals($responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testPostWithRecipientList() + /** + * Test a POST request using a stored recipient list ID. + * + * Why: Ensures that when a list_id is used, individual recipient formatting is bypassed. + * How: Provides a list_id in the payload and verifies the POST request is successful without recipient validation errors. + */ + public function testPostWithRecipientList(): void { $postTransmissionPayload = $this->postTransmissionPayload; $postTransmissionPayload['recipients'] = ['list_id' => 'SOME_LIST_ID']; - $responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); - $responseBodyMock = Mockery::mock(); + $responseMock = Mockery::mock(ResponseInterface::class); + $responseBodyMock = Mockery::mock(StreamInterface::class); $responseBody = ['results' => 'yay']; $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($responseMock); $responseMock->shouldReceive('getStatusCode')->andReturn(200); $responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); $responseBodyMock->shouldReceive('__toString')->andReturn(json_encode($responseBody)); - $response = $this->resource->transmissions->post(); + $response = $this->resource->transmissions->post($postTransmissionPayload); - $this->assertEquals($responseBody, $response->getBody()); + $this->assertEquals($responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testDelete() + /** + * Test a DELETE request to the transmissions endpoint. + * + * Why: Verifies that the Transmission resource correctly delegates DELETE requests to the SparkPost client. + * How: Mocks a successful DELETE request and verifies the response matches the expected mock data. + * + * @throws SparkPostException + */ + public function testDelete(): void { - $responseMock = Mockery::mock('Psr\Http\Message\ResponseInterface'); - $responseBodyMock = Mockery::mock(); + $responseMock = Mockery::mock(ResponseInterface::class); + $responseBodyMock = Mockery::mock(StreamInterface::class); $responseBody = ['results' => 'yay']; $this->clientMock->shouldReceive('sendRequest')-> - once()-> - with(Mockery::type('GuzzleHttp\Psr7\Request'))-> - andReturn($responseMock); + once()-> + with(Mockery::type(RequestInterface::class))-> + andReturn($responseMock); $responseMock->shouldReceive('getStatusCode')->andReturn(200); $responseMock->shouldReceive('getBody')->andReturn($responseBodyMock); @@ -185,13 +234,22 @@ public function testDelete() $response = $this->resource->transmissions->delete($this->getTransmissionPayload); - $this->assertEquals($responseBody, $response->getBody()); + $this->assertEquals($responseBody, $response->getBodyDecoded()); $this->assertEquals(200, $response->getStatusCode()); } - public function testFormatPayload() + /** + * Test the payload formatting logic. + * + * Why: Verifies the complex transformation of transmission payloads into the format required by the API. + * How: Passes a raw payload to formatPayload() and asserts the output matches a predefined correctly formatted JSON structure. + */ + public function testFormatPayload(): void { - $correctFormattedPayload = json_decode('{"content":{"from":{"name":"Sparkpost Team","email":"postmaster@sendmailfor.me"},"subject":"First Mailing From PHP","text":"Congratulations, {{name}}!! You just sent your very first mailing!","headers":{"CC":"avi.goldman@sparkpost.com"}},"substitution_data":{"name":"Avi"},"recipients":[{"address":{"name":"Vincent","email":"vincent.song@sparkpost.com"}},{"address":{"email":"test@example.com"}},{"address":{"email":"emely.giraldo@sparkpost.com","header_to":"\"Vincent\" "}},{"address":{"email":"avi.goldman@sparkpost.com","header_to":"\"Vincent\" "}}]}', true); + $correctFormattedPayload = json_decode( + '{"content":{"from":{"name":"Sparkpost Team","email":"postmaster@sendmailfor.me"},"subject":"First Mailing From PHP","text":"Congratulations, {{name}}!! You just sent your very first mailing!","headers":{"CC":"avi.goldman@sparkpost.com"}},"substitution_data":{"name":"Avi"},"recipients":[{"address":{"name":"Vincent","email":"vincent.song@sparkpost.com"}},{"address":{"email":"test@example.com"}},{"address":{"email":"emely.giraldo@sparkpost.com","header_to":"\"Vincent\" "}},{"address":{"email":"avi.goldman@sparkpost.com","header_to":"\"Vincent\" "}}]}', + true + ); $formattedPayload = $this->resource->transmissions->formatPayload($this->postTransmissionPayload); $this->assertEquals($correctFormattedPayload, $formattedPayload);