Add Middleware/Interceptor System for Request/Response Transformation#71
Add Middleware/Interceptor System for Request/Response Transformation#71
Conversation
✅ Deploy Preview for fetch-php canceled.
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive middleware/interceptor system for request/response transformation to the Fetch HTTP client library. The implementation follows the chain-of-responsibility pattern, enabling developers to inject custom logic for authentication, logging, caching, rate limiting, and other cross-cutting concerns without manually wrapping every HTTP call.
Key Changes:
- Introduces a
MiddlewareInterfacecontract for implementing custom middleware withhandle(RequestInterface, callable)signature - Implements a priority-based
MiddlewarePipelineclass that executes middleware in order with support for request/response modification and short-circuiting - Adds
SupportsMiddlewaretrait providing fluent methods (middleware(),addMiddleware(),prependMiddleware(),when(),unless()) for middleware management
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
src/Fetch/Interfaces/MiddlewareInterface.php |
Defines the contract for middleware implementations with request/response transformation capabilities |
src/Fetch/Interfaces/ClientHandler.php |
Extends the client handler interface with middleware management method signatures |
src/Fetch/Http/MiddlewarePipeline.php |
Core pipeline implementation managing middleware execution order with priority-based sorting |
src/Fetch/Http/ClientHandler.php |
Integrates the SupportsMiddleware trait into the main client handler |
src/Fetch/Concerns/SupportsMiddleware.php |
Provides middleware management methods with lazy initialization and conditional application support |
src/Fetch/Concerns/PerformsHttpRequests.php |
Integrates middleware execution into the request flow with PSR-7 request conversion and body handling |
tests/Unit/MiddlewarePipelineTest.php |
Comprehensive test coverage for pipeline behavior including execution order, modification, and short-circuiting |
tests/Unit/SupportsMiddlewareTest.php |
Tests for middleware management methods including priority handling and conditional application |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $body->rewind(); | ||
| $bodyContents = $body->getContents(); | ||
| $body->rewind(); |
There was a problem hiding this comment.
The code calls $body->rewind() multiple times (lines 250, 252) without checking if the stream is seekable. Non-seekable streams (like php://input in some contexts) will throw an exception when rewind() is called.
Consider checking if the stream is seekable before rewinding:
$body = $request->getBody();
if ($body->getSize() > 0) {
if ($body->isSeekable()) {
$body->rewind();
}
$bodyContents = $body->getContents();
if ($body->isSeekable()) {
$body->rewind();
}
// ... rest of the logic
}Alternatively, since you're converting the body to a string anyway, you might not need to rewind it back after reading.
| $body->rewind(); | |
| $bodyContents = $body->getContents(); | |
| $body->rewind(); | |
| if ($body->isSeekable()) { | |
| $body->rewind(); | |
| } | |
| $bodyContents = $body->getContents(); | |
| if ($body->isSeekable()) { | |
| $body->rewind(); | |
| } |
| /** | ||
| * Set multiple middleware at once. | ||
| * | ||
| * @param array<MiddlewareInterface|array{middleware: MiddlewareInterface, priority: int}> $middleware | ||
| * @return $this | ||
| */ | ||
| public function middleware(array $middleware): self; |
There was a problem hiding this comment.
Similar to the trait, the interface documentation should clarify that middleware() replaces existing middleware:
/**
* Set multiple middleware at once, replacing any existing middleware.
*
* Note: This replaces the entire middleware pipeline. Use addMiddleware()
* to add middleware without removing existing ones.
*
* @param array<MiddlewareInterface|array{middleware: MiddlewareInterface, priority: int}> $middleware
* @return $this
*/| * @param callable $next The next middleware or final handler | ||
| * @return \Fetch\Interfaces\Response|PromiseInterface<\Fetch\Interfaces\Response> The response or promise | ||
| */ | ||
| public function handle(RequestInterface $request, callable $next): Response|PromiseInterface; |
There was a problem hiding this comment.
Missing import statement for the Response interface. The return type uses Response but there's no corresponding use statement. Add this import at the top of the file:
use Fetch\Interfaces\Response;Alternatively, use the fully qualified name in the type hint:
public function handle(RequestInterface $request, callable $next): \Fetch\Interfaces\Response|PromiseInterface;| /** | ||
| * The middleware pipeline instance. | ||
| */ | ||
| protected ?MiddlewarePipeline $middlewarePipeline = null; |
There was a problem hiding this comment.
When the handler is cloned on line 159 of sendRequest(), the $middlewarePipeline property (which is an object reference) will be shallow-copied. This means both the original handler and the cloned handler will share the same middleware pipeline instance. If middleware is added to one handler, it will affect the other.
This could lead to unexpected behavior where middleware added to a cloned handler affects the original handler. Consider implementing a __clone() method in the ClientHandler class or the trait:
public function __clone(): void
{
if ($this->middlewarePipeline !== null) {
$middleware = $this->middlewarePipeline->getMiddleware();
$this->middlewarePipeline = new MiddlewarePipeline($middleware);
}
}| /** | ||
| * Conditionally add middleware. | ||
| * | ||
| * @param bool $condition The condition to check | ||
| * @param callable $callback Callback that receives $this and should add middleware | ||
| * @return $this | ||
| */ | ||
| public function when(bool $condition, callable $callback): self; |
There was a problem hiding this comment.
[nitpick] Similar to the trait, the interface PHPDoc should clarify the callback signature for better type safety and IDE support:
/**
* Conditionally add middleware.
*
* @param bool $condition The condition to check
* @param callable(static): void $callback Callback that receives $this for adding middleware
* @return $this
*/| public function __construct(array $middleware = []) | ||
| { | ||
| foreach ($middleware as $item) { | ||
| if ($item instanceof MiddlewareInterface) { | ||
| $this->middleware[] = ['middleware' => $item, 'priority' => 0]; | ||
| } else { | ||
| $this->middleware[] = $item; | ||
| } | ||
| } | ||
|
|
||
| $this->sortMiddleware(); | ||
| } |
There was a problem hiding this comment.
The constructor accepts arrays with ['middleware' => ..., 'priority' => ...] format but doesn't validate the structure. If an array is passed without the 'middleware' key, it will be added to the stack as-is, which will cause errors later when trying to access $item['middleware'] in the buildPipeline() method.
Consider adding validation:
public function __construct(array $middleware = [])
{
foreach ($middleware as $item) {
if ($item instanceof MiddlewareInterface) {
$this->middleware[] = ['middleware' => $item, 'priority' => 0];
} elseif (is_array($item) && isset($item['middleware']) && $item['middleware'] instanceof MiddlewareInterface) {
$this->middleware[] = [
'middleware' => $item['middleware'],
'priority' => $item['priority'] ?? 0
];
} else {
throw new \InvalidArgumentException(
'Middleware must be an instance of MiddlewareInterface or an array with "middleware" key'
);
}
}
$this->sortMiddleware();
}| /** | ||
| * Conditionally add middleware. | ||
| * | ||
| * @param bool $condition The condition to check | ||
| * @param callable $callback Callback that receives $this and should add middleware | ||
| * @return $this |
There was a problem hiding this comment.
[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.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…tion, remove error_log Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…ering Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…ed config Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…PHPDoc tags Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…and clean up instantiation syntax
…n, null handling, and tests Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
…t and error handling
…t method; update response type hints in caching methods
…, fix cloning Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Applied all changes from the code review in commit d971e7b:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Pull Request
Description
What does this PR do?
Adds a middleware pipeline system for request/response transformation, enabling interceptor-like patterns similar to JavaScript fetch interceptors or Laravel HTTP middleware. This allows automatic authentication token management, logging, caching, rate limiting, and custom business logic injection without manually wrapping every call.
Fixes #(issue_number)
Type of Change
Changes Made
MiddlewareInterface- Contract for middleware implementations withhandle(RequestInterface $request, callable $next)signatureMiddlewarePipeline- Priority-based middleware execution using chain-of-responsibility pattern with input validationSupportsMiddlewaretrait - Addsmiddleware(),addMiddleware(),prependMiddleware(),when(),unless()methods to ClientHandlerPerformsHttpRequests::sendRequest()flowClientHandlerinterface with middleware method signatures__clone()method to ensure cloned handlers have independent middleware pipelinesUsage:
Testing
Test Environment:
32 new tests covering pipeline execution order, priority sorting, request/response modification, short-circuiting, conditional middleware, constructor validation, and clone independence.
Documentation
callable(static): void)Checklist
Additional Notes
middleware()replaces existing middleware; useaddMiddleware()to append without replacingInvalidArgumentExceptionfor invalid itemsOriginal prompt
This section details on the original issue you should resolve
<issue_title>Middleware/Interceptor System</issue_title>
<issue_description>## Summary
Add a middleware pipeline system for request/response transformation, similar to JavaScript fetch interceptors or Laravel HTTP middleware.
Motivation
Currently, there's no way to globally modify requests or responses without manually wrapping every call. A middleware system would enable:
Proposed API