Я пытаюсь использовать черту в качестве ключа для моих контроллеров ресурсов Laravel.
Метод контроллера:
public function store(CreateCommentRequest $request, Commentable $commentable)
В которой Commentable
является типом типа trait, который используют мои модели 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
, я получаю следующую ошибку:
Осветить \ Контракты \ Container \ BindingResolutionException
Цель [App \ Models \ Morphs \ Commentable] не является реальной.
Это вообще возможно?
Таким образом, вы хотите избежать дублирования кода как для контроллеров ресурсов, так и для выполнения и быть немного СУХОЙ. Хорошо.
Как сказал Мэтью, вы не можете вводить черты характера, и именно по этой причине вы получаете ошибку разрешения привязки. Помимо этого, даже если бы это было свойство typehintable, контейнер был бы смущен, какой модели он должен создать, поскольку есть две доступные модели Commentable
. Но мы поговорим позже.
Часто бывает хорошей практикой иметь интерфейс для сопровождения признака. Помимо того факта, что интерфейсы можно вводить с клавиатуры, вы придерживаетесь принципа разделения интерфейса, который «при необходимости» является хорошей практикой.
interface Commentable { public function comments(); } class Order extends Model implements Commentable { use Commentable; // ... }
Теперь, когда это тип. Давайте перейдем к проблеме путаницы в контейнере.
Контейнер 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); });
В основном вы CommentController
контейнер, когда 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) .
Несмотря на то, что переопределяющие параметры метода работают в конструкторах просто отлично, это просто не для других методов! Конструкторы – это особые случаи .
Мы могли бы просто отказаться от типов в родительском и дочернем классах и продолжить свою жизнь с помощью простых идентификаторов. Но в этом случае, поскольку неявная привязка модели 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(); }
Код вашего контроллера:
# 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
model и всех {fulfillment}
s в App\Fulfillment
.
Мы могли бы изменить первое решение для использования параметров маршрута вместо сегментов URL. Но нет причин делать это вручную, когда Ларавел предоставил его нам.
Да, я знаю, я тоже не чувствую себя хорошо.
Вы не можете набирать черты .
Однако вы можете набирать интерфейсы. Таким образом, вы можете создать интерфейс, который требует методов из этого признака и решить эту проблему. Затем ваши классы реализуют этот интерфейс, и вы должны быть в порядке.
EDIT: Как любезно отметил @Stefan, все же, вероятно, будет сложно разрешить интерфейс для конкретного класса, потому что ему придется решать разные классы при разных обстоятельствах. Вы можете получить доступ к запросу у поставщика услуг и использовать путь для определения того, как его решить, но я немного сомневаюсь в этом. Я думаю, что включение их в отдельные контроллеры и использование свойств наследования / характеристик для совместного использования общих функций может быть лучше, поскольку методы в каждом контроллере могут набирать намек на требуемый объект и затем передавать их эквивалентному методу родительского элемента.