Контроллеры

Symfony следует философии "худых контроллеров и толстых моделей". Это означает, что контроллеры должны содержать только тонкий слой склеивающего кода, необходимого для координации разных частей приложения.

Ваши методы контроллера должны просто вызывать другие сервисы, запускать при необходимости какие-то события и потом возвращать ответ, но они не должны содержать какой-либо настоящей бизнес-логики. Если же она есть, извлеките её из контроллера и поместите в сервис.

Best Practice

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

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

И так как ваши контроллеры должны быть "худыми" и содержать не более, чем пару строк склеивающего кода, часы, потраченные на их отделение от вашего фреймворка не принесёт вам пользы. Количество потраченного времени не стоит выгоды.

В дополнение, использоваие аннотаций для маршрутизации, кеширования и безопасности облегчает конфигурацию. Вам не нужно просматривать десятки файлов, созданных в разных форматах (YAML, XML, PHP): вся конфигурация именно там, где вам надо и использует только один формат.

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

Именование действий контроллера

Best Practice

Не добавляйте к методам действий контроллера суффикс Action.

Первые версии Symfony требовали, чтобы имена методов заканчивались на Action (например, newAction(), showAction()). Этот суффикс стал необязательным, когда были представлены аннотации для контроллеров. В современных приложениях Symfony этот суффикс не требуется и не рекомендуется, так что вы можете смело его удалить.

Конфигурация маршрутизации

Чтобы загрузить маршруты, определённые как аннотации, в ваших контроллерах, добавьте следующую конфигурацию к главному файлу коннфигурации маршрутизации:

1
2
3
4
# config/routes.yaml
контроллеры:
    resource: '../src/Controller/'
    type:     annotation

Эта конфигурация загрузит аннотации из любого контроллера, который хранится внутри каталога src/Controller/ и даже из его подкаталогов. Так что если ваше приложение определяет много контроллеров, абсолютно нормально реорганизовать их в подкаталоги:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<your-project>/
├─ ...
└─ src/
   ├─ ...
   └─ Controller/
      ├─ DefaultController.php
      ├─ ...
      ├─ Api/
      │  ├─ ...
      │  └─ ...
      └─ Backend/
         ├─ ...
         └─ ...

Конфигурация шаблонов

Best Practice

Не используйте аннотацию @Template для конфигурации шаблона, используемого контроллером.

Аннотация @Template полезна, но также задействует некоторую магию. Мы не думаем, что её преимущества стоят магии, поэтому рекомендуем её не использовать.

В большинстве времени, @Template используется без параметров, что усложняет понимание того, какой шаблон отображается. Он также делает для новичков менее очевидным тот факт,что контроллер должен всегда возвращать объект Response (кроме случаев, когда вы используете уровень просмотра).

Как выглядит контроллер

Учитывая всё это, вот пример того, как должен выглядеть контроллер для домашней страницы нашего приложения:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace App\Controller;

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/", name="homepage")
     */
    public function index()
    {
        $posts = $this->getDoctrine()
            ->getRepository(Post::class)
            ->findLatest();

        return $this->render('default/index.html.twig', [
            'posts' => $posts,
        ]);
    }
}

Вызов сервисов

Если вы расширяете базовый класс AbstractController, вы не можете получить доступ к сервисам напрямую из контейнера через $this->container->get() или $this->get(). Но вместо этого, вам нужно использовать внедрение зависимости, чтобы вызвать сервисы: легче всего это сделать через типизирование аргументов матода действия:

Best Practice

Не используйте $this->get() или $this->container->get(), чтобы вызвать сервисы из контейнера. Вместо этого используйте внедрение зависимости.

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

Использование ParamConverter

Если вы используете Doctrine, то вы можете опционально использовать ParamConverter для автиматического запроса всей страницы и передачи её в качестве аргумента вашему контроллеру.

Best Practice

Используйте фокус с ParamConverter, чтобы автоматически запросить сущности Doctrine, когда это просто и удобно.

Например:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use App\Entity\Post;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function show(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', [
        'post' => $post,
        'delete_form' => $deleteForm->createView(),
    ]);
}

Обычно, вы ожидаете аргумент $id к show(). Вместо этого, создав новый аргумент ($post) и типизировав его классом Post (который является сущностью Doctrine), ParamConverter автоматически запрашивает объект, свойство $id которого совпадает со значением {id}. Он также отобразит страницу 404, если Post не может быть найден.

Когда всё становится более продвинутым

Вышеописанный пример работает без конфигурации, так как имя заполнителя {id} совпадает с именем свойства сущности. Если это не так, или если у вас более сложная логика, то легче будет просто запросить сущность вручную. В нашм приложении, у нас складывается такая ситуация в CommentController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * @Route("/comment/{postSlug}/new", name="comment_new")
 */
public function new(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository(Post::class)
        ->findOneBy(['slug' => $postSlug]);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

Вы также можете использовать конфигурацию @ParamConverter, которая безагранично гибкая:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use App\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/comment/{postSlug}/new", name="comment_new")
 * @ParamConverter("post", options={"mapping"={"postSlug"="slug"}})
 */
public function new(Request $request, Post $post)
{
    // ...
}

Суть в этом: шорткат ParamConverter идеален для простых ситуаций. Но вам не стоит забывать, что запрос сущностей напрямую тоже очень прост.

Пред- и пост- якоря

Если вам нужно выполнить некоторый код до или после выполнения ваших контроллеров, вы можете использовать компонент EventDispatcher, чтобы установить фильтры до и после.


Далее: Templates

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.