diff --git a/README.md b/README.md index 362cf64..4cfb55c 100644 --- a/README.md +++ b/README.md @@ -359,19 +359,66 @@ class YourApi extends Api ### Event Listeners -- [`addPostRequestHandler`](#addpostrequesthandler) -- [`addResponseContentsHandler`](#addresponsecontentshandler) +- [`addPreRequestListener`](#addprerequestlistener) +- [`addPostRequestListener`](#addpostrequestlistener) +- [`addResponseContentsListener`](#addresponsecontentslistener) - [Event Priority](#event-priority) - [Event Propagation](#event-propagation) -#### `addPostRequestHandler` +#### `addPreRequestListener` -The `addPostRequestHandler` method is used to add a handler function that is executed after a request has been made. -This handler function can be used to inspect the request and response data that was sent to, and received from, the API. +The `addPreRequestListener` method is used to add a function that is called before a request, and all handled data, has been made. This event listener will be applied to every API request. ```php -$this->addPostRequestHandler(callable $handler, int $priority = 0): self; +$this->addPreRequestListener(callable $listener, int $priority = 0): self; +``` + +For example: + +```php +use ProgrammatorDev\Api\Api; +use ProgrammatorDev\Api\Event\PreRequestEvent; + +class YourApi extends Api +{ + public function __construct() + { + // a PreRequestEvent is passed as an argument + $this->addPreRequestListener(function(PreRequestEvent $event) { + $request = $event->getRequest(); + + if ($request->getMethod() === 'POST') { + // do something for all POST requests + // ... + } + }); + } + + // ... +} +``` + +Available event methods: + +```php +$this->addPreRequestListener(function(PreRequestEvent $event) { + // get request data + $request = $event->getRequest(); + // ... + // set request data + $event->setRequest($request); +}); +``` + +#### `addPostRequestListener` + +The `addPostRequestListener` method is used to add a function that is called after a request has been made. +This function can be used to inspect the request and response data that was sent to, and received from, the API. +This event listener will be applied to every API request. + +```php +$this->addPostRequestListener(callable $listener, int $priority = 0): self; ``` For example, you can use this event listener to handle API errors: @@ -384,13 +431,8 @@ class YourApi extends Api { public function __construct() { - // ... - // a PostRequestEvent is passed as an argument - $this->addPostRequestHandler(function(PostRequestEvent $event) { - // request data is also available - // $request = $event->getRequest(); - + $this->addPostRequestListener(function(PostRequestEvent $event) { $response = $event->getResponse(); $statusCode = $response->getStatusCode(); @@ -410,13 +452,27 @@ class YourApi extends Api } ``` -#### `addResponseContentsHandler` +Available event methods: + +```php +$this->addPostRequestListener(function(PostRequestEvent $event) { + // get request data + $request = $event->getRequest(); + // get response data + $response = $event->getResponse(); + // ... + // set response data + $event->setResponse($response); +}); +``` + +#### `addResponseContentsListener` -The `addResponseContentsHandler` method is used to manipulate the response that was received from the API. +The `addResponseContentsListener` method is used to manipulate the response that was received from the API. This event listener will be applied to every API request. ```php -$this->addResponseContentsHandler(callable $handler, int $priority = 0): self; +$this->addResponseContentsListener(callable $handler, int $priority = 0): self; ``` For example, if the API responses are JSON strings, you can use this event listener to decode them into arrays: @@ -429,10 +485,8 @@ class YourApi extends Api { public function __construct() { - // ... - // a ResponseContentsEvent is passed as an argument - $this->addResponseContentsHandler(function(ResponseContentsEvent $event) { + $this->addResponseContentsListener(function(ResponseContentsEvent $event) { // get response contents and decode json string into an array $contents = $event->getContents(); $contents = json_decode($contents, true); @@ -453,6 +507,18 @@ class YourApi extends Api } ``` +Available event methods: + +```php +$this->addResponseContentsListener(function(ResponseContentsEvent $event) { + // get response body contents data + $contents = $event->getContents(); + // ... + // set contents + $event->setContents($contents); +}); +``` + #### Event Priority It is possible to add multiple listeners for the same event and set the order in which they will be executed. @@ -471,14 +537,14 @@ class YourApi extends Api // but the second is executed first (higher priority) even though it was added after // executed last (lower priority) - $this->addResponseContentsHandler( - handler: function(PostRequestEvent $event) { ... }, + $this->addResponseContentsListener( + listener: function(PostRequestEvent $event) { ... }, priority: 0 ); // executed first (higher priority) - $this->addResponseContentsHandler( - handler: function(PostRequestEvent $event) { ... }, + $this->addResponseContentsListener( + listener: function(PostRequestEvent $event) { ... }, priority: 10 ); } @@ -498,13 +564,13 @@ class YourApi extends Api { public function __construct() { - $this->addResponseContentsHandler(function(PostRequestEvent $event) { + $this->addResponseContentsListener(function(PostRequestEvent $event) { // stop propagation so future listeners of this event will not be called $event->stopPropagation(); }); // this listener will not be called - $this->addResponseContentsHandler(function(PostRequestEvent $event) { + $this->addResponseContentsListener(function(PostRequestEvent $event) { // ... }); } @@ -682,7 +748,7 @@ class YourApi extends Api $pool = new FilesystemAdapter(); // file-based cache adapter with a 1-hour default cache lifetime - $this->setClientBuilder( + $this->setCacheBuilder( new CacheBuilder( pool: $pool, ttl: 3600 diff --git a/src/Api.php b/src/Api.php index 366ec69..29be174 100644 --- a/src/Api.php +++ b/src/Api.php @@ -13,6 +13,7 @@ use ProgrammatorDev\Api\Builder\Listener\CacheLoggerListener; use ProgrammatorDev\Api\Builder\LoggerBuilder; use ProgrammatorDev\Api\Event\PostRequestEvent; +use ProgrammatorDev\Api\Event\PreRequestEvent; use ProgrammatorDev\Api\Event\ResponseContentsEvent; use ProgrammatorDev\Api\Exception\ConfigException; use ProgrammatorDev\Api\Helper\StringHelperTrait; @@ -67,6 +68,8 @@ public function request( throw new ConfigException('A base URL must be set.'); } + $this->configurePlugins(); + if (!empty($this->queryDefaults)) { $query = \array_merge($this->queryDefaults, $query); } @@ -75,15 +78,24 @@ public function request( $headers = \array_merge($this->headerDefaults, $headers); } - $this->configurePlugins(); - $uri = $this->buildUri($path, $query); - $request = $this->buildRequest($method, $uri, $headers, $body); + $request = $this->createRequest($method, $uri, $headers, $body); + + // pre request listener + $request = $this->eventDispatcher->dispatch(new PreRequestEvent($request))->getRequest(); + + // request $response = $this->clientBuilder->getClient()->sendRequest($request); - $this->eventDispatcher->dispatch(new PostRequestEvent($request, $response)); + // post request listener + $response = $this->eventDispatcher->dispatch(new PostRequestEvent($request, $response))->getResponse(); + // always rewind the body contents in case it was used in the PostRequestEvent + // otherwise it would return an empty string + $response->getBody()->rewind(); $contents = $response->getBody()->getContents(); + + // response contents listener return $this->eventDispatcher->dispatch(new ResponseContentsEvent($contents))->getContents(); } @@ -236,16 +248,23 @@ public function setAuthentication(?Authentication $authentication): self return $this; } - public function addPostRequestHandler(callable $handler, int $priority = 0): self + public function addPreRequestListener(callable $listener, int $priority = 0): self + { + $this->eventDispatcher->addListener(PreRequestEvent::class, $listener, $priority); + + return $this; + } + + public function addPostRequestListener(callable $listener, int $priority = 0): self { - $this->eventDispatcher->addListener(PostRequestEvent::class, $handler, $priority); + $this->eventDispatcher->addListener(PostRequestEvent::class, $listener, $priority); return $this; } - public function addResponseContentsHandler(callable $handler, int $priority = 0): self + public function addResponseContentsListener(callable $listener, int $priority = 0): self { - $this->eventDispatcher->addListener(ResponseContentsEvent::class, $handler, $priority); + $this->eventDispatcher->addListener(ResponseContentsEvent::class, $listener, $priority); return $this; } @@ -274,7 +293,7 @@ private function buildUri(string $path, array $query = []): string return $uri; } - private function buildRequest( + private function createRequest( string $method, string $uri, array $headers = [], diff --git a/src/Event/PostRequestEvent.php b/src/Event/PostRequestEvent.php index 251eecf..d5c1f74 100644 --- a/src/Event/PostRequestEvent.php +++ b/src/Event/PostRequestEvent.php @@ -10,7 +10,7 @@ class PostRequestEvent extends Event { public function __construct( private readonly RequestInterface $request, - private readonly ResponseInterface $response + private ResponseInterface $response ) {} public function getRequest(): RequestInterface @@ -22,4 +22,9 @@ public function getResponse(): ResponseInterface { return $this->response; } + + public function setResponse(ResponseInterface $response): void + { + $this->response = $response; + } } \ No newline at end of file diff --git a/src/Event/PreRequestEvent.php b/src/Event/PreRequestEvent.php new file mode 100644 index 0000000..cd01e78 --- /dev/null +++ b/src/Event/PreRequestEvent.php @@ -0,0 +1,23 @@ +request; + } + + public function setRequest(RequestInterface $request): void + { + $this->request = $request; + } +} \ No newline at end of file diff --git a/src/Event/ResponseContentsEvent.php b/src/Event/ResponseContentsEvent.php index 350ce3b..da42560 100644 --- a/src/Event/ResponseContentsEvent.php +++ b/src/Event/ResponseContentsEvent.php @@ -6,12 +6,9 @@ class ResponseContentsEvent extends Event { - private mixed $contents; - - public function __construct($contents) - { - $this->contents = $contents; - } + public function __construct( + private mixed $contents + ) {} public function getContents(): mixed { diff --git a/tests/Integration/ApiTest.php b/tests/Integration/ApiTest.php index 9a7881f..3e9fa2a 100644 --- a/tests/Integration/ApiTest.php +++ b/tests/Integration/ApiTest.php @@ -21,7 +21,7 @@ class ApiTest extends AbstractTestCase { private const BASE_URL = 'https://base.com/url'; - private $class; + private Api $api; private Client $mockClient; @@ -30,11 +30,11 @@ protected function setUp(): void parent::setUp(); // create anonymous class - $this->class = new class extends Api {}; + $this->api = new class extends Api {}; // set mock client $this->mockClient = new Client(); - $this->class->setClientBuilder(new ClientBuilder($this->mockClient)); + $this->api->setClientBuilder(new ClientBuilder($this->mockClient)); } public function testSetters() @@ -45,26 +45,26 @@ public function testSetters() 'authenticate' => $this->createMock(RequestInterface::class) ]); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->setClientBuilder(new ClientBuilder()); - $this->class->setCacheBuilder(new CacheBuilder($pool)); - $this->class->setLoggerBuilder(new LoggerBuilder($logger)); - $this->class->setAuthentication($authentication); - - $this->assertSame(self::BASE_URL, $this->class->getBaseUrl()); - $this->assertInstanceOf(ClientBuilder::class, $this->class->getClientBuilder()); - $this->assertInstanceOf(CacheBuilder::class, $this->class->getCacheBuilder()); - $this->assertInstanceOf(LoggerBuilder::class, $this->class->getLoggerBuilder()); - $this->assertInstanceOf(Authentication::class, $this->class->getAuthentication()); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->setClientBuilder(new ClientBuilder()); + $this->api->setCacheBuilder(new CacheBuilder($pool)); + $this->api->setLoggerBuilder(new LoggerBuilder($logger)); + $this->api->setAuthentication($authentication); + + $this->assertSame(self::BASE_URL, $this->api->getBaseUrl()); + $this->assertInstanceOf(ClientBuilder::class, $this->api->getClientBuilder()); + $this->assertInstanceOf(CacheBuilder::class, $this->api->getCacheBuilder()); + $this->assertInstanceOf(LoggerBuilder::class, $this->api->getLoggerBuilder()); + $this->assertInstanceOf(Authentication::class, $this->api->getAuthentication()); } public function testRequest() { $this->mockClient->addResponse(new Response(body: MockResponse::SUCCESS)); - $this->class->setBaseUrl(self::BASE_URL); + $this->api->setBaseUrl(self::BASE_URL); - $response = $this->class->request( + $response = $this->api->request( method: 'GET', path: '/path' ); @@ -77,7 +77,7 @@ public function testMissingBaseUrl() $this->expectException(ConfigException::class); $this->expectExceptionMessage('A base URL must be set.'); - $this->class->request( + $this->api->request( method: 'GET', path: '/path' ); @@ -85,32 +85,32 @@ public function testMissingBaseUrl() public function testQueryDefaults() { - $this->class->addQueryDefault('test', true); - $this->assertTrue($this->class->getQueryDefault('test')); + $this->api->addQueryDefault('test', true); + $this->assertTrue($this->api->getQueryDefault('test')); - $this->class->removeQueryDefault('test'); - $this->assertNull($this->class->getQueryDefault('test')); + $this->api->removeQueryDefault('test'); + $this->assertNull($this->api->getQueryDefault('test')); } public function testHeaderDefaults() { - $this->class->addHeaderDefault('X-Test', true); - $this->assertTrue($this->class->getHeaderDefault('X-Test')); + $this->api->addHeaderDefault('X-Test', true); + $this->assertTrue($this->api->getHeaderDefault('X-Test')); - $this->class->removeHeaderDefault('X-Test'); - $this->assertNull($this->class->getHeaderDefault('X-Test')); + $this->api->removeHeaderDefault('X-Test'); + $this->assertNull($this->api->getHeaderDefault('X-Test')); } public function testCache() { $pool = $this->createMock(CacheItemPoolInterface::class); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->setCacheBuilder(new CacheBuilder($pool)); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->setCacheBuilder(new CacheBuilder($pool)); $pool->expects($this->once())->method('save'); - $this->class->request( + $this->api->request( method: 'GET', path: '/path' ); @@ -120,13 +120,13 @@ public function testLogger() { $logger = $this->createMock(LoggerInterface::class); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->setLoggerBuilder(new LoggerBuilder($logger)); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->setLoggerBuilder(new LoggerBuilder($logger)); // request + response log $logger->expects($this->exactly(2))->method('info'); - $this->class->request( + $this->api->request( method: 'GET', path: '/path' ); @@ -137,9 +137,9 @@ public function testCacheWithLogger() $pool = $this->createMock(CacheItemPoolInterface::class); $logger = $this->createMock(LoggerInterface::class); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->setCacheBuilder(new CacheBuilder($pool)); - $this->class->setLoggerBuilder(new LoggerBuilder($logger)); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->setCacheBuilder(new CacheBuilder($pool)); + $this->api->setLoggerBuilder(new LoggerBuilder($logger)); // request + response + cache log $logger->expects($this->exactly(3))->method('info'); @@ -147,7 +147,7 @@ public function testCacheWithLogger() // error suppression to hide expected warning of null cache item in CacheLoggerListener // https://docs.phpunit.de/en/10.5/error-handling.html#ignoring-issue-suppression // TODO maybe allow user to add cache listeners to CacheBuilder and create a mock? - @$this->class->request( + @$this->api->request( method: 'GET', path: '/path' ); @@ -159,42 +159,56 @@ public function testAuthentication() 'authenticate' => $this->createMock(RequestInterface::class) ]); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->setAuthentication($authentication); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->setAuthentication($authentication); $authentication->expects($this->once())->method('authenticate'); - $this->class->request( + $this->api->request( method: 'GET', path: '/path' ); } - public function testPostRequestHandler() + public function testPreRequestListener() { - $this->class->setBaseUrl(self::BASE_URL); - $this->class->addPostRequestHandler(fn() => throw new \Exception('TestMessage')); + $this->api->setBaseUrl(self::BASE_URL); + $this->api->addPreRequestListener(fn() => throw new \Exception('TestMessage')); $this->expectException(\Exception::class); $this->expectExceptionMessage('TestMessage'); - $this->class->request( + $this->api->request( method: 'GET', path: '/path' ); } - public function testResponseHandler() + public function testPostRequestListener() + { + $this->api->setBaseUrl(self::BASE_URL); + $this->api->addPostRequestListener(fn() => throw new \Exception('TestMessage')); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('TestMessage'); + + $this->api->request( + method: 'GET', + path: '/path' + ); + } + + public function testResponseContentsListener() { $this->mockClient->addResponse(new Response(body: MockResponse::SUCCESS)); - $this->class->setBaseUrl(self::BASE_URL); - $this->class->addResponseContentsHandler(function(ResponseContentsEvent $event) { + $this->api->setBaseUrl(self::BASE_URL); + $this->api->addResponseContentsListener(function(ResponseContentsEvent $event) { $contents = json_decode($event->getContents(), true); $event->setContents($contents); }); - $response = $this->class->request( + $response = $this->api->request( method: 'GET', path: '/path' ); @@ -204,7 +218,7 @@ public function testResponseHandler() public function testBuildPath() { - $path = $this->class->buildPath('/path/{parameter1}/multiple/{parameter2}', [ + $path = $this->api->buildPath('/path/{parameter1}/multiple/{parameter2}', [ 'parameter1' => 'with', 'parameter2' => 'parameters' ]);