Роутинг

13 мая 2018 г.

Роуты (или, по-русски, маршруты) определяются в файлах в каталоге routes - эти файлы Laravel загружает автоматически. В частности, для веб-интерфейса используется файл routes/web.php. Этот файл назначен в группу middleware web, предоставляющую сохранение сессии и защиту CSRF. Роуты из файла routes/api.php принадлежат к middleware api и состояние не сохраняют. Роуты из routes/web.php доступны по урлам из браузера. Определим в этом файле простейший роут в нашем установленном здесь приложении.

Route::get('hi', function() {
    return 'Здорово, человечество!';
});

Фактически, мы создали первую страницу нашего сайта. Правда, она абсолютно пуста, за исключением нашего приветствия, но все же это - страница, доступная по адресу http://laravel/hi.

Если же нам нужно отдавать html (как правило так и есть), то лучше положить его в отдельный файл (представление), а не отдавать одной строкой. Такой роут у нас уже есть, именно он и отдает нам сейчас представление resources/views/welcome.blade.php в качестве главной страницы. Но выглядит он пока что по-старому.

Route::get('/', function () {
    return view('welcome');
});

С версии 5.5 возможен сокращенный синтаксис.

Route::view('/', 'welcome');

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

Route::view('/', 'welcome', ['name' => 'Sergey']);

Если же нам нужна не статическая страница, а некая обработка данных, то это уже будет происходить в контроллере (который будет отдавать представление),  Например, если у нас есть контроллер UserController, метод index которого будет получать из базы и выводить нам список пользователей сайта, и мы хотим, чтобы этот список был доступен по адресу http://laravel/users, то пропишем следующий роут.

Route::get('/users', 'UserController@index');

Сервис-провайдером RouteServiceProvider роуты из разных файлов распределены по группам: из файла routes/web.php - в группу web, из routes/api.php - в группу api. При этом для группы api, определен префикс api, т.е. все роуты из файла api.php будут иметь этот префикс. Мы вольны поменять его здесь (в методе mapApiRoutes) на другой, либо вообще убрать. Также можно добавить какой-нибудь префикс и для веб-роутов, если нужно.

Роутер предоставляет методы для любого HTTP-запроса.

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

Если необходимо, чтобы роут был доступен по нескольким типам запросов, то используем метод match.

Route::match(['get, 'post'], '/', $callback);

Если же роут должен быть доступен по любому типу запроса, то используем метод any.

Route::any('hi', $callback);

Для перенаправления используется метод redirect, принимающий третьим аргументом код ответа сервера.

Route::redirect('/here', '/there', 301);

Параметры роутов

Часто роуты необходимо разбирать, т.е. вытащить из сегмента роута id юзера, например, чтобы показать профиль именно этого юзера, а не кого-то другого. Естественно, роутинг в Laravel предоставляет нам такую возможность.

Route::get('user/{id}', function($id) {
    return "Юзер с id $id";
});

Параметров может быть сколько угодно. В коллбэк (либо в контроллер) они передаются в том же порядке, в котором определены в строке урла (в первом параметре), т.е., в данном случае {post} идет в $postId, а {comment} - в $commentId.

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});

Или в контроллер

Route::get('users/{country_id}/{city_id}', UserController@index);

А в контроллере

function index($country_id, $city_id) {
    //
}

В последнем примере мы явно имеем дело с опциональными параметрами, потому что иногда мы можем не захотеть фильтровать юзеров по странам и городам, а получить сразу всех. ОК, делаем так.

Route::get('users/{country_id?}/{city_id?}', UserController@index);

А в контроллере

function index($country_id = null, $city_id = null) {
    //
}

Параметры роута часто должны отвечать некоему формату. Например, id - целое число. а username у нас должен состоять исключительно из букв латинского алфавита. Проверить соответствие параметра формату нам поможет метод where.

Route::get('user/{username}', function ($name) {
    //
})->where('name', '[A-Za-z]+');
Route::get('user/{id}', function ($id) {
    //
})->where('id', '[0-9]+');
Route::get('user/{id}/{username}', function ($name) {
    //
})->where(['id' => '[0-9]+', 'name' => '[A-Za-z]+']);

Почти наверняка параметр id везде в нашем приложении будет целым числом. Поэтому гораздо удобнее было бы определить его где-то один раз глобально, чем писать в каждом роуте. Такая возможность в Laravel есть - метод pattern. А использовать мы его можем в методе boot провайдера RouteServiceProvider.

    /**
     * Define your route model bindings, pattern filters, etc.
     *
     * @return void
     */
    public function boot()
    {
        Route::pattern('id', '[0-9]+');

        parent::boot();
    }

Теперь мы можем определить роут как обычно

Route::get('user/{id}', function($id) {
    //
});

но отработает он лишь если id - целое число.

Именованные роуты

Именованные роуты позволяют удобно сгенерировать урл к ним, или произвести перенаправление. Назначить имя для роута можно методом name.

Route::get('user/profile', function () {
    //
})->name('profile');

Теперь при необходимости указать URL профайла юзера в приложении используем хелпер route.

$url = route('profile');

А сгенерировать редирект можно так.

return redirect()->route('profile');

Вторым аргументом в хелпер route можно передать массив параметров.

Route::get('user/{id}/profile', function ($id) {
    //
})->name('profile');

$url = route('profile', ['id' => 1]);

Иногда необходимо проверить, соответствует ли текущий роут требуемому. Для этого есть метод named. Например, в middleware (подробнее разберем их позже) мы можем сделать так.

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    if ($request->route()->named('profile')) {
        //
    }

    return $next($request);
}

Ну а получить информацию о текущем роуте можно методами currentcurrentRouteName и currentRouteAction, возвращающими, соответственно, экземпляр класса роута, имя роута (для именованных) и выполняемый метод контроллера.

Группы роутов

Если некоторое количество роутов использует общие атрибуты (middleware, пространства имен и т.д.), то мы можем объединить их в группу, чтобы применить эти атрибуты один раз к группе, а не каждому роуту в отдельности. Массив атрибутов передаются первым аргументом в метод Route::group().

Также есть способ более явно передать отдельно middleware и отдельно пространства имен с помощью методов middlware и namespace соответственно, вызываемых до вызова метода group. Middleware выполняются в порядке указания в массиве.

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Использует middleware first и second
    });

    Route::get('user/profile', function () {
        // Использует middleware first и second
    });
});
Route::namespace('Admin')->group(function () {
    // Контроллеры с пространством имен "App\Http\Controllers\Admin"
});

В последнем примере учитываем тот факт, что RouteServiceProvider включает файлы роутов уже с определенным пространством имен (по умолчанию App\Http\Controllers).

Метод domain позволяет объединить в группу роуты из поддомена. Причем поддомены могут быть параметризированы, так же, как и обычные урлы.

Route::domain('{account}.myapp.com')->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Метод prefix позволяет задать для группы единый префикс. Например, для группы роутов из админки можно задать префикс admin.

Route::prefix('admin')->group(function () {
    Route::get('users', function () {
        // соответствует урлу "/admin/users"
    });
});

Метод name позволяет задать префикс группе для именованных роутов.

Route::name('admin.')->group(function () {
    Route::get('users', function () {
        // роуту присвоено имя "admin.users"
    })->name('users');
});

Привязка модели к роуту

Если мы передаем id некоторой модели в роут, то с большой вероятностью мы это делаем не просто так, а чтобы получить экземпляр этой модели. Laravel позволяет нам в этом случае не заниматься глупостями, а сразу привязывать модель к роуту. Для этого достаточно в коллбэке или контроллере типизировать соответствующий параметру аргумент. Заметим, что в данном случае параметр должен называться точно так же, как и аргумент (в данном случае user).

Route::get('api/users/{user}', function (App\User $user) {
    return $user->email;
});

Здесь роутер автоматически создаст экземпляр модели App\User с id, полученным в параметре user. Если же нам нужно вытащить модель не по id, а по иному столбцу (использую это слово в предположении, что наша модель берет данные из соответствующей таблицы базы данных), то в соответствующей модели прописываем метод getRouteKeyName, где и определяем необходимый столбец. Например, если в той же модели App\User прописать следующее, то параметр роута user будет соответствовать столбцу username.

/**
 * Get the route key for the model.
 *
 * @return string
 */
public function getRouteKeyName()
{
    return 'username';
}

Еще один вариант привязки модели, более явный - использование метода model роутера. Привязка в этом случае делается в методе boot в RouteServiceProvider.

public function boot()
{
    parent::boot();

    Route::model('customer', \App\User::class);
}

А затем стандартно объявляем роут. При этом, в отличие от неяной привязки, мы уже не обязаны одинаково обзывать аргумент коллбэка (или метода контроллера) и параметр роута.

Route::get('profile/{customer}', function (App\User $user) {
    //
});

Более гибко привязать модель можно методом bind, во второй аргумент которого мы передаем коллбэк, в аргумент которого, в свою очередь попадает значение параметра роута (указываемого в первом аргументе для bind). В следующем примере показан метод boot из RouteServiceProvider. В нем мы привязали параметр роута user к свойству name ( и через него к столбцу name таблицы юзеров из базы данных).

public function boot()
{
    parent::boot();

    Route::bind('user', function ($value) {
        return App\User::where('name', $value)->first() ?? abort(404);
    });
}

Здесь использованы незнакомые нам пока методы модели where и first. Забегая вперед: первый формирует условие WHERE sql-запроса (либо аналогичное ему, если не sql...), а второй выбирает лишь одну, первую, запись). Хелпер abort прерывает выполнение приложения и возвращает 404-ю страницу. Ну и, возможно, не все еще знают об операторе PHP ??, появившемся в 7-й версии. Он проверяет левую часть на null: если не null, то возвращается левая часть (переменная или результат выражения); если null - то альтернатива, стоящая справа (в данном случае 404-я страница).

Установка ограничений

Laravel предоставляет middleware throttle для ограничения количества запросов к данному роуту за определенное время. Этот middleware принимает два параметра: допустимое количество запросов и время в минутах. В следующем примере устанавливается ограничение в 60 запросов за 1 минуту для аутентифицированного юзера.

Route::middleware('auth:api', 'throttle:60,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

Количество запросов может устанавливаться динамически, в зависимости от установленного свойства модели User. В следующем примере модель User имеет свойство rate_limit, которое мы передаем в middleware throttle.

Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});