fix: ensure Filter receives current HTTP request when resolved from DI container - #31
Conversation
…I container Fixes #30 - Request parameters not included in filter object pulled from DI container When using dependency injection in controllers like: public function index(PostFilter $postFilter) The filter methods were not being called because Laravel's DI container was creating a new empty Request instance instead of using the current HTTP request. Changes: - Add bindIf() in FilterableServiceProvider to ensure Request::class resolves to the current HTTP request ($app['request']) - Add comprehensive test suite for DI container resolution scenarios The fix ensures that DI-resolved filters behave identically to manually instantiated filters using new PostFilter(request()).
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical dependency injection issue where Filter classes resolved from Laravel's DI container (e.g., in controller method injection) were receiving an empty Request instance instead of the current HTTP request. The fix adds a binding in FilterableServiceProvider that ensures Request::class resolves to the current request ($app['request']), enabling filters to properly access request parameters when using DI.
Key Changes:
- Adds
registerFilterBindings()method inFilterableServiceProviderthat bindsRequest::classto the current request - Introduces comprehensive integration test suite with 12 tests covering various DI resolution scenarios
- Regression tests verify DI-resolved filters behave identically to manually instantiated filters
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/Filterable/Providers/FilterableServiceProvider.php |
Adds packageRegistered() hook and registerFilterBindings() method using bindIf() to bind Request::class to current request |
tests/Integration/FilterDependencyInjectionTest.php |
New comprehensive test suite with 12 tests covering DI resolution scenarios, multiple request types (GET/POST/JSON), multiple parameters, empty requests, and regression tests comparing DI vs manual instantiation |
| public function test_filter_injection_works_with_other_dependencies(): void | ||
| { | ||
| // Simulate a request with filter parameters | ||
| $request = Request::create('/posts', 'GET', [ | ||
| 'name' => 'John', | ||
| ]); | ||
|
|
||
| $this->app->instance('request', $request); | ||
| $this->app->instance(Request::class, $request); | ||
|
|
||
| // Bind cache and logger so they get injected | ||
| $cache = $this->app->make(Cache::class); | ||
| $logger = $this->app->make(LoggerInterface::class); | ||
|
|
||
| $this->app->when(FilterWithDependencies::class) | ||
| ->needs(Cache::class) | ||
| ->give(fn () => $cache); | ||
|
|
||
| $this->app->when(FilterWithDependencies::class) | ||
| ->needs(LoggerInterface::class) | ||
| ->give(fn () => $logger); | ||
|
|
||
| // Create a filter with additional dependencies injected | ||
| $filter = $this->app->make(FilterWithDependencies::class); | ||
|
|
||
| // The filter should have access to the request parameters | ||
| $filterables = $filter->getFilterables(); | ||
|
|
||
| $this->assertArrayHasKey('name', $filterables); | ||
| $this->assertEquals('John', $filterables['name']); | ||
|
|
||
| // Cache and logger should be injected when explicitly bound | ||
| $this->assertTrue($filter->hasCacheHandler()); | ||
| $this->assertTrue($filter->hasLogger()); | ||
| } |
There was a problem hiding this comment.
[nitpick] This test verifies that dependencies are injected but doesn't verify that the filter actually works correctly when applied to a query. While the test confirms getFilterables() returns the expected parameters and that cache/logger are present, it doesn't apply the filter to ensure the query filtering logic works with the injected dependencies. Consider adding an assertion like:
// Verify the filter actually works when applied
$results = MockFilterable::query()->filter($filter)->get();
$this->assertCount(2, $results); // John Doe and Bob Johnson contain 'John'
$this->assertTrue($results->every(fn ($r) => str_contains($r->name, 'John')));| public function __construct( | ||
| Request $request, | ||
| ?Cache $cache = null, | ||
| ?LoggerInterface $logger = null | ||
| ) { | ||
| parent::__construct($request, $cache, $logger); | ||
| } |
There was a problem hiding this comment.
The constructor in FilterWithDependencies is redundant as it simply passes all parameters to the parent Filter constructor without modification. According to the custom coding guidelines, the abstract Filter class already accepts Request, optional Cache, and optional LoggerInterface in its constructor. This redundant constructor can be removed entirely - the parent constructor will be called automatically with the same signature.
| $this->app['request']->merge(['name' => 'John']); | ||
| $filter1 = $this->app->make(MockFilter::class); | ||
|
|
||
| // Modify request (simulating a different request) | ||
| $newRequest = Request::create('/posts', 'GET', ['name' => 'Jane']); | ||
| $this->app->instance('request', $newRequest); | ||
| $this->app->instance(Request::class, $newRequest); |
There was a problem hiding this comment.
This test uses inconsistent approaches for setting request parameters: $this->app['request']->merge() for the first request (line 227) but $this->app->instance() for the second request (lines 232-233). This inconsistency could make the test behavior unclear. For consistency and predictability, use the same approach for both - prefer using $this->app->instance() for both cases as it's more explicit about replacing the entire request instance.
| /** | ||
| * Register contextual bindings for Filter classes. | ||
| * | ||
| * This ensures that when a Filter subclass is resolved from the DI container, | ||
| * it receives the current HTTP request instance rather than an empty Request. | ||
| */ |
There was a problem hiding this comment.
The docblock says "Register contextual bindings for Filter classes" but the implementation uses a global binding with bindIf(), not a contextual binding. Contextual bindings in Laravel use $this->app->when(SomeClass::class)->needs(...), while this is a global binding. Consider updating the docblock to:
/**
* Register global bindings for Filter dependencies.
*
* This ensures that when a Filter subclass is resolved from the DI container,
* it receives the current HTTP request instance rather than an empty Request.
*/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 open a new pull request to apply changes based on the comments in this thread |
|
@Thavarshan I've opened a new pull request, #32, to work on those changes. Once the pull request is ready, I'll request review from you. |
…nsistent request setup, remove redundant constructor Co-authored-by: Thavarshan <10804999+Thavarshan@users.noreply.github.com>
Summary
Fixes #30 - Request parameters not included in filter object pulled from DI container in controller
The Problem
When using dependency injection in Laravel controllers like:
The filter methods were not being called because the
Requestobject passed toPostFilterwhen created by the DI container was empty.Manual instantiation worked correctly:
The Fix
Added a binding in
FilterableServiceProviderthat ensuresRequest::classresolves to the current HTTP request ($app['request']) rather than creating a new empty Request instance:Tests Added
Created comprehensive test suite
tests/Integration/FilterDependencyInjectionTest.phpwith 12 tests covering:Verification