Один и тот же контроллер ресурсов Laravel для нескольких маршрутов



Я пытаюсь использовать признак в качестве typehint для моих контроллеров ресурсов Laravel.



Метод контроллера:



public function store(CreateCommentRequest $request, Commentable $commentable)


, в котором Commentable является чертой типа, которую используют мои красноречивые модели.



Признак Commentable выглядит так:



namespace AppModelsMorphs;

use AppComment;

trait Commentable
{
/**
* Get the model's comments.
*
* @return IlluminateDatabaseEloquentRelationsMorphMany
*/
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
цель [модели приложения\МорфCommentable] не материализуемый.




Возможно ли это вообще?

830   2  

2 ответов:

Таким образом, вы хотите избежать дублирования кода для контроллеров ресурсов заказа и выполнения и быть немного сухим. Хорошо.

Черты не могут быть типизированы

Как заявил Мэтью , вы не можете печатать черты, и это причина, по которой вы получаете ошибку разрешения привязки. Кроме того, даже если бы он был typehintable, контейнер был бы запутан, какую модель он должен создать, поскольку есть две доступные модели Commentable. Но мы к этому еще вернемся. позже.

Интерфейсы наряду с признаками

Часто хорошей практикой является наличие интерфейса для сопровождения признака. Помимо того, что интерфейсы могут быть типизированы, вы придерживаетесь принципа сегрегации интерфейса , который, "если это необходимо", является хорошей практикой.

interface Commentable 
{
    public function comments();
}

class Order extends Model implements Commentable
{
    use Commentable;

    // ...
}

Теперь, когда это typehintable. Давайте перейдем к проблеме путаницы с контейнерами.

Контекстуальная привязка

Контейнер фреймворк 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 требуется экземпляр 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, мы можем просто использовать класс lone CommentController. Явная привязка модели ларавеля работает путем сопоставления заполнителей маршрута (например, {order}) классам моделей или логикам пользовательского разрешения. Таким образом, в то время как мы используем наш единый 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}s будут разрешены в \App\Order модель и все {fulfillment}s в App\Fulfillment.

Мы могли бы изменить первое решение, чтобы использовать параметры маршрута вместо сегментов URL. Но нет никакой причины делать это вручную, когда Ларавель предоставил нам его.


Да, я знаю, мне тоже нехорошо.

Вы не можете печатать черты.

Тем не менее, вы можете печатать интерфейсы. Таким образом, вы можете создать интерфейс, который требует методов из признака, и решить эту проблему. Тогда пусть ваши классы реализуют этот интерфейс, и вы должны быть в порядке.

EDIT: как любезно указал @Stefan, по-прежнему, вероятно, будет трудно разрешить интерфейс к конкретному классу, потому что он должен будет разрешаться к различным классам при различных обстоятельствах. Вы можете получить доступ к запросу в поставщике услуг и использовать путь, чтобы определить, как решить его, но я немного сомневаюсь в этом. Я думаю, что лучше поместить их в отдельные контроллеры и использовать наследование/черты для совместного использования общей функциональности, так как методы в каждом контроллере могут вводить намек на требуемый объект, а затем передавать их эквивалентному родительскому методу.

Comments

    Ничего не найдено.