A lot of pieces to this so here’s the meat. Code very slightly tweaked for brevity.
Extended class:
<?php namespace AppHttp; use IlluminateHttpRequest as LaravelRequest; class Request extends LaravelRequest { }
Middleware:
<?php namespace AppHttpMiddleware; use AppHttpRequest as CustomizedRequest; use Closure; use IlluminateContractsFoundationApplication; use IlluminateHttpRequest; class CustomizeRequest { protected $app; protected $customizedRequest; public function __construct(Application $app, CustomizedRequest $customizedRequest){ $this->app = $app; $this->customizedRequest = $customizedRequest; } public function handle(Request $request, Closure $next){ $this->app->instance( 'request', Request::createFrom($request, $this->customizedRequest); ); return $next($this->customizedRequest); } }
Routes:
Route::get('/books1/{id}',[BookController::class, 'frontend1']); Route::get('/books2/{id}',[BookController::class, 'frontend2']);
Controller:
<?php namespace AppHttpControllers; use AppModelsBook; class BookController extends Controller { public function frontend1(IlluminateHttpRequest $request){ dump($request); dump($request->all()); dump($request->route('id')); return Book::all(); } public function frontend2(AppHttpRequest $request){ dump($request); dump($request->all()); dump($request->route('id')); return Book::all(); } }
The /books1/5?foo=bar
and frontend1()
path works. $request
is populated as expected.
The /books2/5?foo=bar
and frontend2()
path is broken. $request
has vast amounts of missing data, like it was instantiated with nothing.
Evidently if I type-hint my subclass instead of the more generic parent, it’s causing some kind of broken instantiation. From an OO perspective I think this should be perfectly fine and I do specifically need my subclass being provided so prefer that type-hint. Is something deep within Laravel tripping this up? Is this some obscure PHP behavior I haven’t seen before?
Advertisement
Answer
This is kind of tricky.
First of all, you need to be familiar with the service container and dependency injection. Here is the full doc: https://laravel.com/docs/8.x/container
When you type hint a class inside a controller method, Laravel will try to understand what it should do with it.
If nothing is registered inside the service container, it will try to make a new instance of it.
IlluminateHttpRequest
is bound as a singleton (https://laravel.com/docs/8.x/container#binding-a-singleton).
While a simple bind will return a new instance at each call, a singleton will always return the exact same instance.
Here is a quick demo:
AppModelsUser::class
is a class that is not explicitly bound.
When you try to resolve it using the service container, it will not find it and will try to make a new instance:
$u1 = app(AppModelsUser::class); // Searching AppModelsUser::class... // Cannot find AppModelsUser::class... // returning new AppModelsUser(); $u2 = app(AppModelsUser::class); // same process again $u3 = app(AppModelsUser::class); // and again // You can check these instances are indeed different by checking their hash: dd( spl_object_hash($u1), // 000000004af5213500000000220f0bc0 (52135) spl_object_hash($u2), // 000000004af5213400000000220f0bc0 (52134) spl_object_hash($u3) // 000000004af5213700000000220f0bc0 (52137) );
But since IlluminateHttpRequest::class
is bound by Laravel, it follows a different path:
$r1 = app(IlluminateHttpRequest::class); // Searching IlluminateHttpRequest::class... // Found it! Bound as a singleton. // returning new IlluminateHttpRequest() and storing the // instance in case it is required again later; $r2 = app(IlluminateHttpRequest::class); // Searching IlluminateHttpRequest::class... // Found it and already called! Returning the stored instance ($r1) $r3 = app(IlluminateHttpRequest::class); // Searching IlluminateHttpRequest::class... // Found it and already called! Returning the stored instance ($r1) // Their hash are the same dd( spl_object_hash($u1), // 0000000011f522cf0000000077704cd1 spl_object_hash($u2), // 0000000011f522cf0000000077704cd1 spl_object_hash($u3) // 0000000011f522cf0000000077704cd1 );
Now, what’s happening?
Under the hood, when a new request is made to your app and before hitting the controller method, Laravel will do a lot of things to prepare the IlluminateHttpRequest
instance.
For instance, it will setup the route resolver inside IlluminateRoutingRouter
:
/** * Return the response for the given route. * * @param IlluminateHttpRequest $request * @param IlluminateRoutingRoute $route * @return SymfonyComponentHttpFoundationResponse */ protected function runRoute(Request $request, Route $route) { // here $request->setRouteResolver(function () use ($route) { return $route; }); // $this->events->dispatch(new RouteMatched($route, $request)); return $this->prepareResponse($request, $this->runRouteWithinStack($route, $request) ); }
Each time Laravel internally call a method like this:
protected function method(Request $request){ // do something to $request }
$request
is always the same instance, because it is bound as a singleton.
We are now in your controller.
public function frontend1(IlluminateHttpRequest $request){ // Searching IlluminateHttpRequest::class... // Found it and already called! // Returning the stored instance that has been prepared through all // Laravel core classes dump($request); dump($request->all()); //well prepared dump($request->route('id')); //well setup return Book::all(); } public function frontend2(AppHttpRequest $request){ // Searching AppHttpRequest::class... // Cannot find AppHttpRequest::class... // returning new AppHttpRequest(); dump($request); dump($request->all()); //nothing dump($request->route('id')); //empty return Book::all(); }
If you are still here, how to solve this problem?
The easiest way is to use a FormRequest
, initially designed to handle form validation, but if you return an empty rules array, you should be able to do everything you did with your custom AppHttpRequest
instance:
<?php namespace AppHttp; use IlluminateFoundationHttpFormRequest; class Request extends FormRequest { public function rules() { return []; } }
Try again, everything should work fine, since this is a feature specially designed to replace the initial IlluminateHttpRequest
object.
The full doc is here: https://laravel.com/docs/8.x/validation#creating-form-requests