FunnyApp: разбираемся с роутингом

После того, как мы сделали обзор конфигурирования приложения, думаю будет логично рассмотреть, а как же устроен роутинг (routing) в каждом из фреймворков. Для этого предлагаю создать несколько страниц:

  • список организаций
  • список пользователей конкретной организации
  • подробная информация о пользователе
  • список категорий портфолио
  • список работ конкретной категории портфолио

Роутинг в Yii

«Из коробки» роуты в Yii выглядят не очень красиво, например, чтобы попасть в action index контроллера company, нужно набрать в адресной строке:

/index.php?r=company/index

Если в файле конфигурации frontend/config/main.php раскомментировать строки:

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'rules' => [],
],

то в адресной строке можно будет уже указать только

/index.php/company/index

Чтобы сделать совсем красиво и убрать из адреса index.php нужно сделай перенаправление всех запросов к нашему сайту на файл index.php. Для Apache это делается добавлением в папку frontend/web файла .htaccess с содержимым:

Options Includes FollowSymLinks
#hide index.php
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php

Теперь мы можем обратиться к

/company/index

и получим содержимое нашего экшена.

Чтобы сделать для экшена иной роут, отличающийся от структуры controller/action, или для создания более сложного правила сопоставления, для этого нужно добавить правило в конфигурацию, в массив rules:

'urlManager' => [
    'rules' => [],
],

О правилах формирования rules и вообще более подробно о роутинге можно почитать в документации:
http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html

Теперь нам нужно создать контроллер и экшен для нашего роута.

Все контроллеры (для frontend части нашего приложения) будут располагаться в папке frontend/controllers. Имеется правило именования контроллеров, они должны иметь структуру названия вида <название контроллера>Controller, то есть в нашем случае это будет CompanyController.php. Внутри должен быть класс с аналогичным названием, который наследуется от базового контроллера yii\web\Controller.

Экшены (public функции класса) тоже имеют своё правило именования: action<название экшена в стиле CamelCase>. В нашем случае это actionIndex. Важно отметить, что если в названии экшена более чем одно слово, например, actionListOfCompanies, то роут будет выглядеть как /company/list-of-companies. То есть CamelCase превращается в название-через-дефис.

Далее рассмотрим роут с параметром, который покажет список пользователей конкретной организации по id этой организации: /company/show/<id: \d+>

Для того, чтобы корректно обработать запрос с параметром (в нашем случае это id сущности), нужно добавить правило (rule) в конфигурацию (о rules мы упоминали выше). Правило для company/show будет выглядеть так:

'rules' => [
    'company/show/<id:\d+>' => 'company/show',
]

Экшен actionShow для данного контроллера должен принимать в качестве параметра переменную $id, которая должна состоять только из цифр. Подобным образом добавим правила и для других схожих роутов.

Чтобы сделать роут для отображения списка сущностей более красивым и понятным, добавим правило:

'companies' => 'company/index',

Теперь список организаций будет доступен по адресу /companies.

Таким образом наш контроллер frontend/controllers/CompanyController.php с учётом нововведений PHP 7 будет иметь вид:

<?php
namespace frontend\controllers;

use yii\web\Controller;

/**
 * Company controller
 */
class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return string
     */
    public function actionIndex(): string
    {
        return 'List of companies';
    }
	
	/**
     * Displays list of users for company.
     *
     * @return string
     */
    public function actionShow(int $id): string
    {
        return 'List of users for company. Company id: ' . $id;
    }
}

Аналогичным образом создадим контроллеры, экшены и правила для роутов:

  • /user/<id: \d+> — подробная информация о пользователе
  • /portfolio/index — список категорий портфолио
  • /portfolio/show/<id: \d+> — список работ конкретной категории портфолио

Теперь рассмотрим, как сделать тоже самое только в рамках Symfony.

Роутинг в Symfony

Существует два возможных варианта, как организовать роутинг в Symfony. Первый вариант, который также идёт «из коробки», это когда в конфигурационном файле app/config/routing.yml содержится лишь такая настройка:

app:
    resource: "@AppBundle/Controller/"
    type:     annotation

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

Какие недостатки я вижу в этом подходе? Во-первых, по роуту далеко не всегда можно легко догадаться в каком контроллере лежит экшен, на который он ведёт. В случае использования IDE можно воспользоваться поиском по части роута или по всему роуту в целом. Если же мы открыли проект, например, через WinSCP и не знаем точно, в каком контроллере лежит экшен, на который ведёт тот или иной роут, то в худшем случае нам придётся пересмотреть все контроллеры, а в рамках большого приложения контроллеров может быть очень много. Во-вторых, этот подход, на мой взгляд, децентрализованный, нет наглядности, затруднительно «окинуть взглядом» группу роутов. По моему мнению более удобный вариант, когда все роуты сосредоточены в одном файле или, в случае большого приложения, разбиты на несколько групп, которые помещены в отдельные файлы, например, группа API. Этот подход мы и будем использовать при разработке FunnyApp в рамках фреймворка Symfony.

Создадим для начала два роута:
1) для списка компаний
2) для просмотра пользователей выбранной компании

Наш файл app/config/routing.yml будет выглядеть следующим образом:

company_list:
    path:      /companies
    defaults:  { _controller: AppBundle:Company:list }

company_show:
    path:      /company/{id}
    defaults:  { _controller: AppBundle:Company:show }
    requirements:
        id: '\d+'

Здесь company_list — это название роута, некоторая метка, которая используется, когда формируется ссылка на данный роут. Значение для ключа path задаёт адрес для роута. В defaults располагается секция, содержащая строку, указывающую в какой бандл (AppBundle), в какой контроллер (Company) и в какой экшен (list) нужно обращаться приложению в случае вызова данного роута. Помимо ключа _controller в данной секции могут быть и другие ключи, например _format: html. Для роута company_show у нас указан параметр {id}, который принимает данный роут, а также присутствует ещё один ключ requirements. Он говорит нам о том, что требуется, чтобы параметр {id} состоял только из цифр. Более подробно про routing можно прочитать в официальной документации тут: http://symfony.com/doc/current/routing.html и тут http://symfony.com/doc/current/components/routing.html

Теперь создадим необходимый контроллер и экшены для наших роутов. Наш контроллер будет располагаться в папке src/AppBundle/Controller, называться CompanyController.php и содержать следующий код:

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return Response
     */
	public function listAction(): Response
    {
        return new Response('List of companies');
    }
	
	/**
     * Displays list of users for company.
     *
     * @return Response
     */
    public function showAction(int $id): Response
    {
        return new Response('List of users for company. Company id: ' . $id);
    }
}

Контроллер должен наследоваться от базового контроллера Symfony\Bundle\FrameworkBundle\Controller\Controller. Правила именования контроллеров гласят, чтобы контроллер назывался следующим образом: <название контроллера>Controller. Экшены должны иметь название <название экшена>Action и возвращать в конечном итоге объект Response из пакета Symfony\Component\HttpFoundation. В нашем случае мы возвращаем данный объект непосредственно, в случае использования представления (view) мы возвращаем из экшена результат работы метода $this->render(), который в конечном счёте тоже возвращает объект Response. Экшен showAction принимает параметр $id, который мы указали для роута company_show.

Аналогичным образом создадим роуты, контроллеры и экшены для страниц:

  • подробная информация о пользователе
  • список категорий портфолио
  • список работ конкретной категории портфолио

Перейдём к рассмотрению роутинга в Laravel.

Роутинг в Laravel

Список роутов, предназначенных для web интерфейса располагается в файле routes/web.php. Чтобы определить некоторый роут требуется воспользоваться фасадом Route. Более подробно о фасадах в Laravel можно почитать в документации: https://laravel.com/docs/5.4/facades

Данный класс имеет ряд методов, из которых на данный момент нам потребуется только метод get(), который в качестве первого параметра принимает адрес роута (как строку), а второй параметр может быть трёх разных типов:

Route::get('companies', function () {
    return 'List of companies';
});
Route::get('companies', 'CompanyController@index');

Route::get('companies', [
    'as' => 'company.index',
    'uses' => 'CompanyController@index'
]);

В первом случае это callback-функция, во втором случае это строка, идентифицирующая экшен, к которому нужно обратиться, а в третьем случае это массив, который состоит из ключа as — название роута и ключа uses — идентификатора экшена. Идентификатор экшена состоит из названия контроллера (с учётом неймспейса относительно папки app/Http/Controllers, то есть, если контроллер OrderController находится в папке app/Http/Controllers/Api, то нам нужно будет записать Api\OrderController), затем идёт знак @, после которого идёт название экшена (метода в указанном контроллера). Никаких ограничений на название файла и класса контроллера не налагается, кроме того, что название файла должно совпадать с названием класса. Суффикс Controller добавляется для улучшения визуальной идентификации класса, как контроллера. На название экшена никаких ограничений также не налагается. Мы будем использовать третий вариант. Добавим также второй роут для отображения списка пользователей указанной компании. Таким образом файл routes/web.php будет выглядеть следующим образом:

<?php

Route::get('companies', [
	'as' => 'company.index',
	'uses' => 'CompanyController@index'
]);

Route::get('company/show/{id}', [
	'as' => 'company.show', 
	'uses' => 'CompanyController@show'
])->where('id', '(\d+)');

Второй роут, который мы добавили, имеет параметр {id}. Чтобы указать, что он должен состоять строго из цифр, вызовем по цепочке метод where(‘id’, ‘(\d+)’), в котором указывается какой параметр и в каком формате должен быть.

Более подробно о роутинге можно прочитать в документации: https://laravel.com/docs/5.4/routing 

Осталось только создать контроллер и соответствующие экшены. В папке app/Http/Controllers создадим файл CompanyController.php со следующим содержимым:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controller;

class CompanyController extends Controller
{
    /**
     * Displays list of companies.
     *
     * @return string
     */
	public function index(): string
    {
        return 'List of companies';
    }
	
	/**
     * Displays list of users for company.
     *
     * @return string
     */
    public function show(int $id): string
    {
        return 'List of users for company. Company id: ' . $id;
    }
}

Чтобы фреймворк распознал класс, как контроллер, он должен наследоваться от Illuminate\Routing\Controller.

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