多个路由的相同 Laravel 资源控制器

Same Laravel resource controller for multiple routes

我正在尝试使用特征作为我的 Laravel 资源控制器的类型提示。

控制器方法:

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

其中 Commentable 是我的 Eloquent 模型使用的特征类型提示。

Commentable 特征如下所示:

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');
    }
}

在我的路由中,我有:

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

订单和履行都可以有注释,因此它们使用相同的控制器,因为代码是相同的。

然而,当我post到order/{order}/comment时,我得到以下错误:

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

这可能吗?

You can't typehint traits.

但是,您可以键入提示界面。因此,您可以创建一个需要特征中的方法的接口并解决它。然后让你的 classes 实现那个接口,你应该没问题。

编辑:正如@Stefan 所指出的,将接口解析为具体的 class 仍然可能很困难,因为在不同的情况下需要解析为不同的 classes .您可以访问服务提供商中的请求并使用路径来确定如何解决它,但我对此有点怀疑。我认为将它们放在单独的控制器中并使用 inheritance/traits 共享公共功能可能是更好的选择,因为每个控制器中的方法可以类型提示所需的对象,然后将它们传递给等效的父方法。

所以你想避免订单和履行资源控制器的重复代码并且有点干。好。

无法对特征进行类型提示

作为 Matthew ,您无法键入提示特征,这就是您遇到绑定解析错误的原因。除此之外,即使它是可类型提示的,容器也会混淆它应该实例化哪个模型,因为有两个 Commentable 模型可用。但是,我们稍后再讲。

接口和特征

拥有一个伴随特征的界面通常是一个好习惯。除了可以对接口进行类型提示这一事实之外,您还要遵守 Interface Segregation 原则,"if needed" 原则是一个很好的做法。

interface Commentable 
{
    public function comments();
}

class Order extends Model implements Commentable
{
    use Commentable;

    // ...
}

现在可以输入提示了。让我们来看看容器混淆问题。

上下文绑定

Laravel的容器支持contextual binding。这是明确告诉它何时以及如何将抽象解析为具体的能力。

您为控制器获得的唯一区别因素是路线。我们需要以此为基础。大致如下:

# 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);
     });

你基本上是在告诉容器 CommentController 需要一个 Commentable 实例,首先检查路由,然后实例化正确的可注释模型。

非上下文绑定也可以:

# 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);
});

错误的工具

我们刚刚通过引入不必要的复杂性消除了重复的控制器代码,这更糟糕。

尽管它有效,但它很复杂、不可维护、非通用且最糟糕的是,它依赖于 URL。它使用了错误的工具来完成工作,而且是完全错误的。

继承

消除这些问题的正确工具就是继承。引入一个抽象的基本评论控制器class并从中扩展两个浅层的。

# App\Http\Controllers\CommentController
abstract class CommentController extends Controller
{
    public function store(CreateCommentRequest $request, Commentable $commentable) {
        // ...
    }

    // All other common methods here...
}

# App\Http\Controllers\OrderCommentController
class OrderCommentController extends CommentController
{
    public function store(CreateCommentRequest $request, Order $commentable) {
        return parent::store($commentable);
    }
}

# App\Http\Controllers\FulfillmentCommentController
class FulfillmentCommentController extends CommentController
{
    public function store(CreateCommentRequest $request, Fulfillment $commentable) {
        return parent::store($commentable);
    }
}

# Routes
Route::resource('order.comment', 'OrderCommentController');
Route::resource('fulfillments.comment', 'FulfillCommentController');

简单、灵活且可维护。

糟糕,语言错误

没那么快:

Declaration of OrderCommentController::store(CreateCommentRequest $request, Order $commentable) should be compatible with CommentController::store(CreateCommentRequest $request, Commentable $commentable).

尽管覆盖方法参数在构造函数中工作得很好,但它根本不适用于其他方法!构造函数是 special cases.

我们可以删除父项和子项中的类型提示 classes 并继续使用普通 ID 的生活。但在那种情况下,由于 Laravel 的隐式模型绑定仅适用于类型提示,因此我们的控制器不会有任何自动模型加载。

好吧,也许在一个更好的世界。

更新:参见 PHP 7.4 对类型差异的支持

显式路由模型绑定

那我们要做什么?

如果我们明确告诉路由器如何加载我们的 Commentable 模型,我们可以只使用单独的 CommentController class。 Laravel 的 explicit model binding 通过将路由占位符(例如 {order})映射到模型 classes 或自定义解析逻辑来工作。因此,当我们使用单个 CommentController 时,我们可以根据订单和履行的路线占位符使用单独的模型或解析逻辑。因此,我们放弃类型提示并依赖占位符。

对于资源控制器,占位符名称取决于您传递给 Route::resource 方法的第一个参数。只需执行 artisan route:list 即可找出答案。

好的,开始吧:

# App\Providers\RouteServiceProvider::boot()
public function boot()
{
    // Map `{order}` route placeholder to the \App\Order model
    $this->app->router->model('order', \App\Order::class);

    // Map `{fulfillment}` to the \App\Fulfilment model
    $this->app->router->model('fulfillment', \App\Fulfilment::class);

    parent::boot();
}

您的控制器代码为:

# App\Http\Controllers\CommentController
class CommentController extends Controller
{
    // Note that we have dropped the typehint here:
    public function store(CreateCommentRequest $request, $commentable) {
        // $commentable is either an \App\Order or a \App\Fulfillment
    }

    // Drop the typehint from other methods as well.
}

并且路由定义保持不变。

它比第一个解决方案更好,因为它不依赖于 URL 段,这些段容易发生变化,而路由占位符很少变化。它也是通用的,因为所有 {order} 都将解析为 \App\Order 模型,所有 {fulfillment} 将解析为 App\Fulfillment

我们可以更改第一个解决方案以利用路由参数而不是 URL 段。但是当 Laravel 已经提供给我们时,没有理由手动进行。


是啊,我知道,我也感觉不舒服。

对于我的案例,我有以下资源:

        Route::resource('books/storybooks', 'BookController');
        Route::resource('books/magazines', 'BookController');

在 php artisan route:cache 之后,它创建了与 'magazine' 模型绑定的路由。

解决方案是在 app/Providers/RouteServiceProvider.php > boot() 方法中添加以下行,在 parent::boot():

之后
        Route::model('magazine', \App\Book::class);

注意单复数。