-
-
Notifications
You must be signed in to change notification settings - Fork 27
Add Middleware/Interceptor System for Request/Response Transformation #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
5c61f33
c29537f
3d07422
621c234
4665e4d
9b25fa1
eba3dd5
d9122a1
5892023
9428c55
95539e4
7ab3d2b
6ef63be
25061d0
798891b
56baf59
af63d60
b094bb5
5a167c4
7e8c750
b477660
74d4128
a556117
e08995e
e956c1a
4feb84c
06de2aa
bd31a7b
446203c
dc4cdeb
2a4042b
8a233c7
c0efed2
e30c597
51aa72c
9bc7a75
d971e7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
| use GuzzleHttp\Exception\RequestException as GuzzleRequestException; | ||
| use GuzzleHttp\Psr7\Request as GuzzleRequest; | ||
| use Matrix\Exceptions\AsyncException; | ||
| use Psr\Http\Message\RequestInterface; | ||
| use React\Promise\PromiseInterface; | ||
|
|
||
| use function Matrix\Support\async; | ||
|
|
@@ -187,14 +188,98 @@ public function sendRequest( | |
| $handler->logRequest($methodStr, $fullUri, $guzzleOptions); | ||
| } | ||
|
|
||
| // Send the request (async or sync) | ||
| // Check if middleware is available and should be used | ||
| if (method_exists($handler, 'hasMiddleware') && $handler->hasMiddleware()) { | ||
| return $handler->executeWithMiddleware($methodStr, $fullUri, $guzzleOptions, $startTime); | ||
| } | ||
|
|
||
| // Send the request (async or sync) without middleware | ||
| if ($handler->isAsync) { | ||
| return $handler->executeAsyncRequest($methodStr, $fullUri, $guzzleOptions); | ||
| } else { | ||
| return $handler->executeSyncRequest($methodStr, $fullUri, $guzzleOptions, $startTime); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Execute the request through the middleware pipeline. | ||
| * | ||
| * @param string $method The HTTP method | ||
| * @param string $uri The full URI | ||
| * @param array<string, mixed> $options The Guzzle options | ||
| * @param float $startTime The request start time | ||
| * @return ResponseInterface|PromiseInterface The response or promise | ||
| */ | ||
| protected function executeWithMiddleware( | ||
| string $method, | ||
| string $uri, | ||
| array $options, | ||
| float $startTime, | ||
| ): ResponseInterface|PromiseInterface { | ||
| // Create a PSR-7 request from the current state | ||
| $body = null; | ||
| if (isset($options['body'])) { | ||
| $body = $options['body']; | ||
| } elseif (isset($options['json'])) { | ||
| $body = json_encode($options['json']); | ||
| } | ||
|
|
||
| $psrRequest = new GuzzleRequest( | ||
| $method, | ||
| $uri, | ||
| $options['headers'] ?? [], | ||
| $body | ||
| ); | ||
|
|
||
| // Define the core handler that will be called after all middleware | ||
| $coreHandler = function (RequestInterface $request) use ($options, $startTime): ResponseInterface|PromiseInterface { | ||
| // Extract method and URI from the (potentially modified) request | ||
| $method = $request->getMethod(); | ||
| $uri = (string) $request->getUri(); | ||
|
|
||
| // Merge any headers from the modified request back into options | ||
| $headers = []; | ||
| foreach ($request->getHeaders() as $name => $values) { | ||
| $headers[$name] = implode(', ', $values); | ||
| } | ||
| $options['headers'] = array_merge($options['headers'] ?? [], $headers); | ||
|
|
||
| // Handle body from modified request | ||
| $body = $request->getBody(); | ||
| if ($body->getSize() > 0) { | ||
| $body->rewind(); | ||
| $bodyContents = $body->getContents(); | ||
| $body->rewind(); | ||
|
|
||
| // Check if it's JSON | ||
| $contentType = $request->getHeaderLine('Content-Type'); | ||
| if (str_contains($contentType, 'application/json')) { | ||
| $decoded = json_decode($bodyContents, true); | ||
| if (json_last_error() === JSON_ERROR_NONE) { | ||
| $options['json'] = $decoded; | ||
| unset($options['body']); | ||
| } else { | ||
| $options['body'] = $bodyContents; | ||
| unset($options['json']); | ||
| } | ||
| } else { | ||
| $options['body'] = $bodyContents; | ||
| unset($options['json']); | ||
| } | ||
| } | ||
|
Comment on lines
+313
to
+340
|
||
|
|
||
| // Execute the actual request | ||
| if ($this->isAsync) { | ||
| return $this->executeAsyncRequest($method, $uri, $options); | ||
| } else { | ||
| return $this->executeSyncRequest($method, $uri, $options, $startTime); | ||
| } | ||
|
Comment on lines
+297
to
+347
|
||
| }; | ||
|
|
||
| // Execute through the middleware pipeline | ||
| return $this->getMiddlewarePipeline()->handle($psrRequest, $coreHandler); | ||
| } | ||
|
|
||
| /** | ||
| * Sends an HTTP request with the specified parameters. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,125 @@ | ||||||||||||||||||||||
| <?php | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| declare(strict_types=1); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace Fetch\Concerns; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| use Fetch\Http\MiddlewarePipeline; | ||||||||||||||||||||||
| use Fetch\Interfaces\ClientHandler; | ||||||||||||||||||||||
| use Fetch\Interfaces\MiddlewareInterface; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Trait for adding middleware support to HTTP clients. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * This trait provides methods for managing and executing middleware | ||||||||||||||||||||||
| * that can transform requests and responses. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| trait SupportsMiddleware | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * The middleware pipeline instance. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| protected ?MiddlewarePipeline $middlewarePipeline = null; | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Set multiple middleware at once. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
|
Thavarshan marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||
| * @param array<MiddlewareInterface|array{middleware: MiddlewareInterface, priority: int}> $middleware | ||||||||||||||||||||||
| * @return $this | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| public function middleware(array $middleware): ClientHandler | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| $this->middlewarePipeline = new MiddlewarePipeline($middleware); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+35
to
+36
|
||||||||||||||||||||||
| $this->middlewarePipeline = new MiddlewarePipeline($middleware); | |
| $pipeline = $this->getMiddlewarePipeline(); | |
| $pipeline->clear(); | |
| foreach ($middleware as $item) { | |
| if ($item instanceof MiddlewareInterface) { | |
| $pipeline->add($item, 0); | |
| } elseif (is_array($item) && isset($item['middleware'], $item['priority'])) { | |
| $pipeline->add($item['middleware'], $item['priority']); | |
| } | |
| } |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The PHPDoc comment for the when() method states "Callback that receives $this and should add middleware", but it doesn't specify what the callback should return. The callback's return value is currently ignored. Consider clarifying this in the documentation:
/**
* Conditionally add middleware.
*
* @param bool $condition The condition to check
* @param callable(static): void $callback Callback that receives $this for adding middleware
* @return $this
*/This makes it clearer that the callback doesn't need to return anything and receives the handler instance as a parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code calls
$body->rewind()multiple times (lines 250, 252) without checking if the stream is seekable. Non-seekable streams (likephp://inputin some contexts) will throw an exception whenrewind()is called.Consider checking if the stream is seekable before rewinding:
Alternatively, since you're converting the body to a string anyway, you might not need to rewind it back after reading.