【问题标题】:Same Laravel resource controller for multiple routes多个路由的相同 Laravel 资源控制器
【发布时间】:2018-02-18 02:07:15
【问题描述】:

我正在尝试使用 trait 作为我的 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')

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

但是,当我发布到 order/{order}/comment 时,我收到以下错误:

Illuminate\Contracts\Container\BindingResolutionException
目标 [App\Models\Morphs\Commentable] 不可实例化。

这可能吗?

【问题讨论】:

  • 看起来自动解决您的依赖关系失败。请提供您的Commentable 文件的代码。
  • @manniL,我已经用 Commentable 的内容更新了描述
  • 好吧,你为什么不使用真正的“评论”模型呢?那行得通:D
  • Commentable 可以是订单或履行
  • 是的,但如果你使用Polymorphic Relationship 应该没问题:)

标签: php laravel laravel-5 dependency-injection dry


【解决方案1】:

因此,您希望避免订单和履行资源控制器的代码重复,并且有点干。很好。

特征不能输入提示

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

特性旁边的接口

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

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。它使用了错误的工具来完成这项工作,这是完全错误的。

继承

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

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

简单、灵活且可维护。

啊,语言错误

没那么快:

OrderCommentController::store(CreateCommentRequest $request, Order $commentable) 的声明应该与 CommentController::store(CreateCommentRequest $request, Commentable $commentable) 兼容。

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

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

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

                          

                                ?更新:查看PHP 7.4对类型差异的支持?

显式路由模型绑定

那我们该怎么办?

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

对于资源控制器,占位符名称取决于您传递给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}s 都将解析为\App\Order 模型,所有{fulfillment}s 将解析为App\Fulfillment

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


是的,我知道,我也不舒服。

【讨论】:

  • 我采用了继承方法,很好的答案。你能举例说明 OrderCommentController 的 store 方法是什么样的吗?我不能输入提示 Order,因为它必须与父方法兼容。
  • @sepehr 不错的可用选项摘要。
  • @sepehr 很好的答案,如果可能的话,我会给 100 票。我选择了路由模型绑定选项,效果很好。
【解决方案2】:

You can't typehint traits.

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

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

【讨论】:

  • 我假设 DIC 应该解析 Commentable。如果 Commentable 是一个接口,多个具体类通过 CommentableTrait 填充接口,那么 DIC 怎么知道要实例化哪个具体类?
  • @Stefan 好点。在这些情况下,将其解析为所需的类可能会很麻烦,除非您访问服务提供者中的请求并根据 URL 以不同的方式解析它,这对我来说有点太 hacky 了。拥有单独的控制器可能更安全。
【解决方案3】:

就我而言,我有以下资源:

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

注意单复数。

【讨论】:

    猜你喜欢
    • 2014-08-07
    • 1970-01-01
    • 2021-05-26
    • 2013-06-04
    • 2014-10-08
    • 1970-01-01
    • 2018-04-16
    • 2013-08-20
    • 2014-07-08
    相关资源
    最近更新 更多