• Час читання ~8 хв
  • 11.08.2023

Як у своїх проєктах на Laravel ви налаштовуєте маршрути для того чи іншого ресурсу? Ви робите це в такий спосіб?

Route::get('/articles/{id}', [ArticlesController::class, 'show']);

Тоді, чи реалізуєте ви це так у своєму ArticlesController?

public function show($id)
{
	$article = Article::findOrFail($id);

	return view('articles.show', [
		'article' => $article,
	]);
}

Якщо це ваш звичайний процес, Laravel пропонує більш ефективну альтернативу під назвою Route Model Binding . Ця функція автоматично отримує відповідну модель і передає її у ваш метод. Все, що вам потрібно зробити, це використовувати ім'я моделі як заповнювач параметра:

Route::get('/articles/{article}', [ArticlesController::class, 'show']);

У ArticlesController ви можете налаштувати його таким чином:

public function show(Article $article) {
    return view('articles.show', [
        'article' => $article
    ]);
}

Таким чином, щоразу, коли ви звертаєтеся до URL-адреси, ви отримуватимете очікувані деталі:

my-blog.local/articles/1

Налаштування ключа

Можуть бути випадки, коли 'id' не є ключем, який ви шукаєте. У нашому випадку ми хотіли б знайти статтю за допомогою її slug. Є два способи підійти до цього:

  1. Specify the key in the placeholder
Route::get('/articles/{article:slug}', [ArticlesController::class, 'show']);

Тепер це буде запитувати модель 'Articles' за slug, а не за id:

Article::where('slug', $value)->firstOrFail();
  1. Define a routeKeyName method in the Model:
class Article extends Model  {
    public function getRouteKeyName()  {
        return 'slug';
    }
}

Перевагою використання є простота використанняgetRouteKeyName. Вам не доведеться вводити {article:slug} текст щоразу, коли ви визначаєте маршрут, який вимагає екземпляра 'Article'.

Умови

запиту Прив'язка моделі маршруту також дозволяє змінювати спосіб виконання модельних запитів. Наприклад, у нашій моделі «Стаття» ми можемо запитувати лише ті статті, які вже були опубліковані. У нинішньому стані навіть чернетки статей можна було б отримати, якщо хтось правильно вгадав слимак майбутньої статті.

Для цього ми можемо перевизначити resolveRouteBinding метод у нашій моделі:

public function resolveRouteBinding($value, $field = null)
{
    return $this->query()
	    ->where($field ?? $this->getRouteKeyName(), $value)
	    ->where('publish_date' , '<=', now())
	    ->first();
}

Хоча ми могли б обійти $field та жорстко закодувати 'slug' як наш ключ, краще повернутися до getRouteKeyName. Це підтримує різні сценарії в нашому додатку та не обмежує нас використанням лише "slug".

Визначення обсягу

Наразі ми відображаємо всі статті, оскільки нам потрібен лише слимак, щоб отримати необхідну інформацію. Але що робити, якщо ми хочемо відобразити статтю під її відповідним автором? Цього можна досягти, просто визначивши новий маршрут, який поєднує ім'я автора та slug статті:

1Route::get('/author/{user:username}/articles/{article:slug}', [AuthorArticlesController::class, 'show']);

У AuthorArticlesController ви повинні зробити наступне:

1public function show(User $user, Article $article)
2{
3    return view('articles.show', [
4        'article' => $article,
5        'author' => $user
6    ]);
7}

Laravel автоматично гарантує, що стаття пов'язана з правильним користувачем. Він робить це, шукаючи зв'язок articles у моделі User і отримуючи його наступним чином:

1$user->articles()->where('slug', $value)->firstOrFail();

This process happens automatically, but&mldr;## Without Визначення обсягу

Але що робити, якщо ви не хочете обмежувати статті лише їхніми авторами? Візьмемо, наприклад, список улюблених читачів. Як це можна відобразити під їхнім ім'ям?

1Route::get(
2	'/read/{user:username}/articles/{article:slug}',
3	[ArticlesController::class, 'show']
4)->withoutScopedBindings();

В цьому випадку можна withoutScopedBindings() використовувати метод. Це гарантує, що статті не будуть охоплені конкретним користувачем, що дозволить отримати ширший доступ.

Користувацькі класи

Також можуть бути випадки, коли ви хочете прив'язати об'єкти, які не є моделями, наприклад, об'єкт UUID.

Припустімо, що ми генеруємо UUID для наших рахунків-фактур:

1Route::get('/invoices/{uuid}', [InvoiceController::class, 'show']);

Ми можемо створити прив'язку UUID, щоб передати його як об'єкт:

1public function show(LazyUuidFromString $uuid) {
2	// do something
3}

Щоб перетворити рядок UUID назад на об'єкт, ми можемо визначити прив'язку в AppServiceProvider:

1class AppServiceProvider extends ServiceProvider
2{
3    public function register(): void
4    {
5        Route::bind('uuid', function ($value) {
6            return Uuid::fromString($value);
7        });
8    }
9}

Route Binding — це потужний інструмент, який можна використовувати у багатьох випадках для спрощення ваших контролерів, що полегшує їх управління та обслуговування.

Як це працює

Давайте розглянемо короткий приклад, щоб зрозуміти, як ми могли б реалізувати це самостійно.

Ми могли б створити нове проміжне програмне забезпечення та встановити прив'язку для будь-якого параметра маршруту з ім'ям щось Route::any('/load/{something}', ...)

 1class AutoBinding
 2{
 3    public function handle(Request $request, Closure $next): Response
 4    {
 5        /** @var Route $route */
 6        $route = $request->route();
 7        if($route->hasParameter('something')) {
 8            $route->setParameter('something', SomethingModel::findByUuid($route->parameter('something')));
 9        }
10        return $next($request);
11    }
12}

Отже, це базова структура, яка нам потрібна для налаштування прив'язки. Тепер давайте подивимося, як Laravel це робить.

Глибоке занурення в прив'язку

маршрутів Якщо ви вивчите масив $middlewareGroups, визначений у app\Http\Kernel.php, ви помітите, що \Illuminate\Routing\Middleware\SubstituteBindings::class проміжне програмне забезпечення використовується як для Інтернету, так і для API. Давайте зануримося в нього:

 1public function handle($request, Closure $next)
 2{
 3    try {
 4        $this->router->substituteBindings($route = $request->route());
 5
 6        $this->router->substituteImplicitBindings($route);
 7    } catch (ModelNotFoundException $exception) {
 8        if ($route->getMissing()) {
 9            return $route->getMissing()($request, $exception);
10        }
11
12        throw $exception;
13    }
14
15    return $next($request);
16}

Метод handle виконує дві функції: substituteBindings і substituteImplicitBindings.

SubstituteBindings

Перша функція, , намагається знайти будь-яке визначене зв'язування (подібно до того, як ми використовували Route::bind() метод у постачальника послуг):

 1// Illuminate\Routing\Router
 2public function substituteBindings($route)
 3{
 4    foreach ($route->parameters() as $key => $value) {
 5        if (isset($this->binders[$key])) {
 6            $route->setParameter($key, $this->performBinding($key, $value, $route));
 7        }
 8    }
 9
10    return $route;
11}

Функція performBinding викликає зворотний виклик, визначений для ваших біндерів:

1protected function performBinding($key, $value, $route)
2{
3    return call_user_func($this->binders[$key], $value, $route);
4}

Цей процес дуже схожий на користувацьку версію, substituteBindingsяку ми розробили разом у проміжному програмному забезпеченні AutoBinding вище.

SubstituteImplicitBindings

Другий метод, substituteImplicitBindings, є дещо складнішим, оскільки він намагається зібрати все необхідне для правильної заміни параметра:

1public function substituteImplicitBindings($route)
2{
3    ImplicitRouteBinding::resolveForRoute($this->container, $route);
4}

Давайте заглибимося в resolveForRoute метод:

 1public static function resolveForRoute($container, $route)
 2{
 3    $parameters = $route->parameters();
 4
 5    $route = static::resolveBackedEnumsForRoute($route, $parameters);
 6
 7    foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {
 8        if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
 9            continue;
10        }
11
12        $parameterValue = $parameters[$parameterName];
13
14        if ($parameterValue instanceof UrlRoutable) {
15            continue;
16        }
17
18        $instance = $container->make(Reflector::getParameterClassName($parameter));
19
20        $parent = $route->parentOfParameter($parameterName);
21
22        $routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
23                    ? 'resolveSoftDeletableRouteBinding'
24                    : 'resolveRouteBinding';
25
26        if ($parent instanceof UrlRoutable &&
27            ! $route->preventsScopedBindings() &&
28            ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
29            $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
30                        ? 'resolveSoftDeletableChildRouteBinding'
31                        : 'resolveChildRouteBinding';
32
33            if (! $model = $parent->{$childRouteBindingMethod}(
34                $parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
35            )) {
36                throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
37            }
38        } elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
39            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
40        }
41
42        $route->setParameter($parameterName, $model);
43    }
44}

У цьому методі багато чого відбувається, тому давайте розіб'ємо його по пунктах, щоб зрозуміти кожен рядок:

Перший рядок отримує масив параметрів з маршруту, потім відбуваються такі кроки:

Resolve Enums

Bindings не тільки пов'язані з моделями, але вони також можуть автоматично визначати значення переліків:

 1protected static function resolveBackedEnumsForRoute($route, $parameters)
 2{
 3    foreach ($route->signatureParameters(['backedEnum' => true]) as $parameter) {
 4        if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
 5            continue;
 6        }
 7
 8        $parameterValue = $parameters[$parameterName];
 9
10        $backedEnumClass = $parameter->getType()?->getName();
11
12        $backedEnum = $backedEnumClass::tryFrom((string) $parameterValue);
13
14        if (is_null($backedEnum)) {
15            throw new BackedEnumCaseNotFoundException($backedEnumClass, $parameterValue);
16        }
17
18        $route->setParameter($parameterName, $backedEnum);
19    }
20
21    return $route;
22}

Метод resolveBackedEnumsForRoute отримує ім'я параметра, а потім намагається отримати значення за допомогою tryFrom(). Якщо він не може визначити значення, він викидає BackedEnumCaseNotFoundException.

Розв'язування прив'язок моделей Після

того, як переліки вирішені, Laravel виконує ще один цикл над усіма параметрами в дії маршруту, щоб отримати всі UrlRoutable об'єкти:

1foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {

Усі моделі в Laravel реалізують інтерфейс.UrlRoutable

Але як тільки він отримує параметри UrlRoutable у циклі, він перевіряє, чи параметр route вже вирішено:

1$parameterValue = $parameters[$parameterName];
2
3if ($parameterValue instanceof UrlRoutable) {
4    continue;
5}

Наступним кроком є вирішення цього нового параметра та заміна його відповідним екземпляром моделі. Отже, Laravel намагається зробити висновок про назву моделі, а потім створює новий об'єкт:

1$instance = $container->make(Reflector::getParameterClassName($parameter));

Наступний рядок отримує батьківський елемент моделі, якщо це необхідно:

1$parent = $route->parentOfParameter($parameterName);

Примітка: 'parent' тут пов'язаний з концепцією 'scoping', яку ми обговорювали раніше.

Далі Laravel визначає, який метод використовувати, виходячи з того, чи хочемо ми отримати скинуті записи чи ні:

1$routeBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
2            ? 'resolveSoftDeletableRouteBinding'
3            : 'resolveRouteBinding';

На цьому етапі Laravel має всю необхідну інформацію. Останні дві частини стосуються лише того, щоб вирішити, чи слід розв'язувати маршрут з екземпляра чи з його батьківського елемента.

Отримання екземпляра від батьківського

елемента Перша умова перевіряє, чи існує батьківський елемент, чи реалізує він UrlRoutable і чи дозволено область видимості. Якщо всі ці умови виконуються, він викличе або resolveChildRouteBinding resolveSoftDeletableChildRouteBindingабо :

 1if ($parent instanceof UrlRoutable &&
 2    ! $route->preventsScopedBindings() &&
 3    ($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
 4    $childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
 5                ? 'resolveSoftDeletableChildRouteBinding'
 6                : 'resolveChildRouteBinding';
 7
 8    if (! $model = $parent->{$childRouteBindingMethod}(
 9        $parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
10    )) {
11        throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
12    }

Потім він виконає виклик parent:

1$model = $parent->{$childRouteBindingMethod}($parameterName, $parameterValue, $route->bindingFieldFor($parameterName))

, який вирішить звичайний виклик відношення на кшталт:

1$user->posts->find($value)

Resolve from the Instance

Отже, якщо немає батьківського елемента або якщо withoutScope це примусово, то Laravel виконає виклик моделі безпосередньо:

1} elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
2    throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
3}

Якщо екземпляр не знайдено, Він викине .ModelNotFoundException

Останнє, що потрібно зробити в кінці, це те, що він замінює параметр

1$route->setParameter($parameterName, $model);

:І це все, що потрібно для прив'язки маршруту в Laravel.

Вдалого кодування!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297