Same Laravel resource controller for multiple rout

2019-04-09 18:05发布

问题:

I am trying to use a trait as a typehint for my Laravel resource controllers.

The controller method:

public function store(CreateCommentRequest $request, Commentable $commentable)

In which the Commentable is the trait typehint which my Eloquent models use.

The Commentable trait looks like this:

namespace App\Models\Morphs;

use App\Comment;

trait Commentable
{
   /**
    * Get the model's comments.
    *
    * @return \Illuminate\Database\Eloquent\Relations\MorphMany
    */
    public function Comments()
    {
        return $this->morphMany(Comment::class, 'commentable')->orderBy('created_at', 'DESC');
    }
}

In my routing, I have:

Route::resource('order.comment', 'CommentController')
Route::resource('fulfillments.comment', 'CommentController')

Both orders and fulfillments can have comments and so they use the same controller since the code would be the same.

However, when I post to order/{order}/comment, I get the following error:

Illuminate\Contracts\Container\BindingResolutionException
Target [App\Models\Morphs\Commentable] is not instantiable.

Is this possible at all?

回答1:

So you want to avoid duplicate code for both order and fulfillment resource controllers and be a bit DRY. Good.

Traits cannot be typehinted

As Matthew stated, you can't typehint traits and that's the reason you're getting the binding resolution error. Other than that, even if it was typehintable, the container would be confused which model it should instantiate as there are two Commentable models available. But, we'll get to it later.

Interfaces alongside traits

It's often a good practice to have an interface to accompany a trait. Besides the fact that interfaces can be typehinted, you're adhering to the Interface Segregation principle which, "if needed", is a good practice.

interface Commentable 
{
    public function comments();
}

class Order extends Model implements Commentable
{
    use Commentable;

    // ...
}

Now that it's typehintable. Let's get to the container confusion issue.

Contexual binding

Laravel's container supports contextual binding. That's the ability to explicitly tell it when and how to resolve an abstract to a concrete.

The only distinguishing factor you got for your controllers, is the route. We need to build upon that. Something along the lines of:

# AppServiceProvider::register()
$this->app
    ->when(CommentController::class)
    ->needs(Commentable::class)
    ->give(function ($container, $params) {
        // Since you're probably utilizing Laravel's route model binding, 
        // we need to resolve the model associated with the passed ID using
        // the `findOrFail`, instead of just newing up an empty instance.

        // Assuming this route pattern: "order|fullfilment/{id}/comment/{id}"
        $id = (int) $this->app->request->segment(2);

        return $this->app->request->segment(1) === 'order'
            ? Order::findOrFail($id)
            : Fulfillment::findOrFail($id);
     });

You're basically telling the container when the CommentController requires a Commentable instance, first check out the route and then instantiate the correct commentable model.

Non-contextual binding will do as well:

# AppServiceProvider::register()
$this->app->bind(Commentable::class, function ($container, $params) {
    $id = (int) $this->app->request->segment(2);

    return $this->app->request->segment(1) === 'order'
        ? Order::findOrFail($id)
        : Fulfillment::findOrFail($id);
});

Wrong tool

We've just eliminated duplicate controller code by introducing unnecessary complexity which is as worse as that.