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

时间:2017-09-09 07:59:26

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

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

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

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

  

照亮\合同\集装箱\ BindingResolutionException
  目标[App \ Models \ Morphs \ Commentable]无法实例化。

这有可能吗?

3 个答案:

答案 0 :(得分:7)

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

特征不能打字

作为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');

简单,灵活和可维护。

Arrrgh,错误的语言

不是那么快:

  

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

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

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

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

显式路由模型绑定

那我们要做什么?

如果我们明确告诉路由器如何加载我们的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}将被解析为\App\Order模型,所有{fulfillment}将被解析为App\Fulfillment

我们可以改变第一个解决方案来利用路由参数而不是URL段。但是当Laravel将它提供给我们时,没有理由手动完成。

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

答案 1 :(得分:1)

You can't typehint traits

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

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

答案 2 :(得分:0)

对于我来说,我有以下资源:

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

php artisan route:cache之后,它将创建与“ magazine”模型绑定的路由。

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

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

请注意单数和复数。