Як у своїх проєктах на 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. Є два способи підійти до цього:
- Specify the key in the placeholder
Route::get('/articles/{article:slug}', [ArticlesController::class, 'show']);
Тепер це буде запитувати модель 'Articles' за slug, а не за id:
Article::where('slug', $value)->firstOrFail();
- 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…## 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.
Вдалого кодування!