Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel 5: Type-hinting a FormRequest class inside a controller that extends from BaseController

I have a BaseController that provides the foundation for most HTTP methods for my API server, e.g. the store method:

BaseController.php

/**
 * Store a newly created resource in storage.
 *
 * @return Response
 */
public function store(Request $request)
{
    $result = $this->repo->create($request);

    return response()->json($result, 200);
}

I then extend on this BaseController in a more specific controller, such as the UserController, like so:

UserController.php

class UserController extends BaseController {

    public function __construct(UserRepository $repo)
    {
        $this->repo = $repo;
    }

}

This works great. However, I now want to extend UserController to inject Laravel 5's new FormRequest class, which takes care of things like validation and authentication for the User resource. I would like to do this like so, by overwriting the store method and using Laravel's type hint dependency injection for its Form Request class.

UserController.php

public function store(UserFormRequest $request)
{
    return parent::store($request);
}

Where the UserFormRequest extends from Request, which itself extends from FormRequest:

UserFormRequest.php

class UserFormRequest extends Request {

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'  => 'required',
            'email' => 'required'
        ];
    }

}

The problem is that the BaseController requires a Illuminate\Http\Request object whereas I pass a UserFormRequest object. Therefore I get this error:

in UserController.php line 6
at HandleExceptions->handleError('2048', 'Declaration of Bloomon\Bloomapi3\Repositories\User\UserController::store() should be compatible with Bloomon\Bloomapi3\Http\Controllers\BaseController::store(Illuminate\Http\Request $request)', '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php', '6', array('file' => '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php')) in UserController.php line 6

So, how can I type hint inject the UserFormRequest while still adhering to the BaseController's Request requirement? I cannot force the BaseController to require a UserFormRequest, because it should work for any resource.

I could use an interface like RepositoryFormRequest in both the BaseController and the UserController, but then the problem is that Laravel no longer injects the UserFormController through its type hinting dependency injection.

like image 309
Tom Avatar asked Sep 09 '25 20:09

Tom


2 Answers

In contrast to many 'real' object oriented languages, this kind of type hinting design in overridden methods is just not possible in PHP, see:

class X {}
class Y extends X {}

class A {
    function a(X $x) {}
}

class B extends A {
    function a(Y $y) {} // error! Methods with the same name must be compatible with the parent method, this includes the typehints
}

This produces the same kind of error as your code. I would just not put a store() method in your BaseController. If you feel that you are repeating code, consider introducing for example a service class or maybe a trait.

Using a service class

Below a solution that makes use of an extra service class. This might be overkill for your situation. But if you add more functionality to the StoringServices store() method (like validation), it could be useful. You can also add more methods to the StoringService like destroy(), update(), create(), but then you probably want to name the service differently.

class StoringService {

    private $repo;

    public function __construct(Repository $repo)
    {
        $this->repo = $repo;
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request)
    {
        $result = $this->repo->create($request);

        return response()->json($result, 200);
    }
}

class UserController {

    // ... other code (including member variable $repo)

    public function store(UserRequest $request)
    {
        $service = new StoringService($this->repo); // Or put this in your BaseController's constructor and make $service a member variable
        return $service->store($request);
    }

}

Using a trait

You can also use a trait, but you have to rename the trait's store() method then:

trait StoringTrait {

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request)
    {
        $result = $this->repo->create($request);

        return response()->json($result, 200);
    }
}

class UserController {

    use {
        StoringTrait::store as baseStore;
    }

    // ... other code (including member variable $repo)

    public function store(UserRequest $request)
    {
        return $this->baseStore($request);
    }

}

The advantage of this solution is that if you do not have to add extra functionality to the store() method, you can just use the trait without renaming and you do not have to write an extra store() method.

Using inheritance

In my opinion, inheritance is not so suitable for the kind of code reuse that you need here, at least not in PHP. But if you want to only use inheritance for this code reuse problem, give the store() method in your BaseController another name, make sure that all classes have their own store() method and call the method in the BaseController. Something like this:

BaseController.php

/**
 * Store a newly created resource in storage.
 *
 * @return Response
 */
protected function createResource(Request $request)
{
    $result = $this->repo->create($request);

    return response()->json($result, 200);
}

UserController.php

public function store(UserFormRequest $request)
{
    return $this->createResource($request);
}
like image 67
Jeroen Noten Avatar answered Sep 12 '25 12:09

Jeroen Noten


You can move your logic from BaseController to trait, service, facade.

You can not override existing function and force it to use different type of argument, it would break stuff. For example, if you later would write this:

function foo(BaseController $baseController, Request $request) {
    $baseController->store($request);
}

It would break with your UserController and OtherRequest because UserController expects UserController, not OtherRequest (which extends Request and is valid argument from foo() perspective).

like image 38
Daniel Antos Avatar answered Sep 12 '25 11:09

Daniel Antos