多个路由的相同 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.
这可能吗?
但是,您可以键入提示界面。因此,您可以创建一个需要特征中的方法的接口并解决它。然后让你的 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);
注意单复数。
我正在尝试使用特征作为我的 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.
这可能吗?
但是,您可以键入提示界面。因此,您可以创建一个需要特征中的方法的接口并解决它。然后让你的 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);
注意单复数。