因此,您希望避免订单和履行资源控制器的代码重复,并且有点干。很好。
特征不能输入提示
作为 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 提供给我们时,没有理由手动进行。
是的,我知道,我也不舒服。