Skip to content
Advertisement

Extended Request missing data when reaching controller with type-hint

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.

Populated Correctly

The /books2/5?foo=bar and frontend2() path is broken. $request has vast amounts of missing data, like it was instantiated with nothing.

Populated Incorrectly

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

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement