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