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

and*_*ard 5 php dependency-injection dry laravel laravel-5

我试图使用特征作为我的Laravel资源控制器的类型提示.

控制器方法:

public function store(CreateCommentRequest $request, Commentable $commentable)
Run Code Online (Sandbox Code Playgroud)

其中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');
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的路由中,我有:

Route::resource('order.comment', 'CommentController')
Route::resource('fulfillments.comment', 'CommentController')
Run Code Online (Sandbox Code Playgroud)

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

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

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

这有可能吗?

sep*_*ehr 9

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

特征不能被打字

正如Matthew 所说,你不能输入提示特征,这就是你得到绑定解析错误的原因.除此之外,即使它是可打字的,容器也会混淆它应该实例化的模型,因为有两种Commentable模型可用.但是,我们稍后会谈到它.

与特征接口

拥有一个伴随特征的界面通常是一种很好的做法.除了可以对接口进行打字这一事实外,您还坚持接口隔离原则,"如果需要",这是一个很好的做法.

interface Commentable 
{
    public function comments();
}

class Order extends Model implements Commentable
{
    use Commentable;

    // ...
}
Run Code Online (Sandbox Code Playgroud)

现在它是可以打字的.让我们来解决容器混淆问题.

上下文绑定

Laravel的容器支持上下文绑定.这是明确告诉它何时以及如何将抽象解析为具体的能力.

您为控制器获得的唯一区别因素是路线.我们需要在此基础上再接再厉.有点像:

# 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);
     });
Run Code Online (Sandbox Code Playgroud)

您基本上在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);
});
Run Code Online (Sandbox Code Playgroud)

错误的工具

我们刚刚通过引入不必要的复杂性来消除重复的控制器代码.

                                   

即使它有效,它也很复杂,不可维护,非泛型,最糟糕的是,它依赖于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');
Run Code Online (Sandbox Code Playgroud)

简单,灵活和可维护.

Arrrgh,语言不对

不是那么快:

宣言OrderCommentController ::店(CreateCommentRequest $请求,命令$ commentable)应与兼容CommentController ::店(CreateCommentRequest $要求,Commentable $ commentable) .

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

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

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

                           

显式路由模型绑定

那么我们要做什么?

如果我们明确告诉路由器如何加载我们的Commentable模型,我们可以使用单独的CommentController类.Laravel的显式模型绑定通过将路径占位符(例如{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();
}
Run Code Online (Sandbox Code Playgroud)

您的控制器代码将是:

# 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.
}
Run Code Online (Sandbox Code Playgroud)

路线定义保持不变.

它比第一个解决方案更好,因为它不依赖于易于改变的URL段,这与很少改变的路径占位符相反.它也是通用的,因为所有{order}s都将被解析为\App\Order模型和所有{fulfillment}s App\Fulfillment.

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


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

  • @sepehr可用选项的很好的总结. (4认同)