Архитектура

Дата обновления перевода 2023-01-16

Архитектура

Вы - мой герой! Кто бы мог подумать, что вы все еще будете здесь, после первых трех частей? Ваши усилия вскоре будут вознаграждены. Первые три части не углублялись в рассмотрение архитектуры фреймворка. Так как она выделяет Symfony из толпы фреймворков, давайте теперь нырнем в мир архитектуры.

Добавление логирования

Новое приложение Symfony микроскопическое: оно по сути состоит просто из системы маршрутизации и контроллера. Но благодаря Flex, установка новых функций проста.

Хотите систему логирования? Не проблема:

1
$ composer require logger

Это устанавливает и конфигурирует (через рецепт) мощную библиотеку Monolog. Чтобы использовать логгер в контроллере, добавьте новый аргумент, типизрованный LoggerInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// src/Controller/DefaultController.php
namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    #[Route('/hello/{name}', methods: ['GET'])]
    public function index(string $name, LoggerInterface $logger): Response
    {
        $logger->info("Saying hello to $name!");

        // ...
    }
}

Вот и всё! Новое сообщение лога будет написано в var/log/dev.log. Путь файла лога или даже другой метод логгирования можно сконфигурировать обновив один из файлов конфигурации, добавленных рецептом.

Сервисы и автомонтирование

Но погодите! Только что случилось что-то очень крутое. Symfony прочла типизирование LoggerInterface и автоматически поняла, что должна передатьнам объект Логгера! Это называется автомонтирование.

Каждая часть работы, которая проделывается в приложении Symfony, осуществляется объектом: объект Логгер логирует, а объект Twig отображает шаблоны. Эти объекты называются сервисами и они являются инструментыми, которые помогаю вам строить богатые функции.

Чтобы сделать жизнь потрясающей, вы можете попросить Symfony передать вам сервис, используя подсказки. Какие другие возможные классы или интерфейсы вы можете использовать? Узнайте, выполнив:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ php bin/console debug:autowiring

  # это только *маленький* пример вывода...

  Описывает экземпляр логгера.
  Psr\Log\LoggerInterface (monolog.logger)

  Запросить стек, контролирующий жизненный цикл запросов.
  Symfony\Component\HttpFoundation\RequestStack (request_stack)

  RouterInterface - это интерфейс, который должны реализовывать все классы Маршрутизатора.
  Symfony\Component\Routing\RouterInterface (router.default)

  [...]

Это только краткое резюме полного списка! И по мере добавления пакетов, этот список также будет расти!

Создание сервисов

Чтобы ваш код был упорядоченым, вы даже можете создать ваши собственные сервисы! Представьте, что вы хотите сгенерировать рандомное приветствие (например, "Привет", "Йо", и др.). Вместо того, чтобы помещать этот код напрямую в контроллер, создайте новый класс:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// src/GreetingGenerator.php
namespace App;

class GreetingGenerator
{
    public function getRandomGreeting(): string
    {
        $greetings = ['Hey', 'Yo', 'Aloha'];
        $greeting = $greetings[array_rand($greetings)];

        return $greeting;
    }
}

Отлично! Вы можете использовать это в вашем контроллере незамедлительно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// src/Controller/DefaultController.php
namespace App\Controller;

use App\GreetingGenerator;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    #[Route('/hello/{name}', methods: ['GET'])]
    public function index(string $name, LoggerInterface $logger, GreetingGenerator $generator): Response
    {
        $greeting = $generator->getRandomGreeting();

        $logger->info("Saying $greeting to $name!");

        // ...
    }
}

Вот и всё! Symfony инстанциирует GreetingGenerator автоматически ипередаст его в качестве аргумента. Но, можем ли мы также переместить логику логгера в GreetingGenerator? Да! Вы можете использовать автомонтирование внутри сервиса, чтобы получить доступ к другим сервисам. Единственное отличие в том, что это делается в конструкторе:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
  // src/GreetingGenerator.php
+ use Psr\Log\LoggerInterface;

  class GreetingGenerator
  {
+     public function __construct(private readonly LoggerInterface $logger)
+     {
+     }

      public function getRandomGreeting(): string
      {
          // ...

+        $this->logger->info('Using the greeting: '.$greeting);

           return $greeting;
      }
  }

Да! Это тоже работает: никакой конфигурации, время не потеряно. Продолжайте писать код!

Расширение Twig и автоконфигурация

Благодаря обработке сервисов Symfony, вы можете расширять Symfony множеством способов, вроде создания подписчика событий или избирателя безопасности для сложных правил авторизации. Давайте добавим в Twig новый фильтр под названием greet. Как? Просто создайте класс, расширяющий AbstractExtension:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
// src/Twig/GreetExtension.php
namespace App\Twig;

use App\GreetingGenerator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class GreetExtension extends AbstractExtension
{
    public function __construct(private readonly GreetingGenerator $greetingGenerator)
    {
    }

    public function getFilters()
    {
        return [
            new TwigFilter('greet', [$this, 'greetUser']),
        ];
    }

    public function greetUser(string $name): string
    {
        $greeting =  $this->greetingGenerator->getRandomGreeting();

        return "$greeting $name!";
    }
}

После создания всего одного файла, вы можете сразу же это использовать:

1
2
3
{# templates/default/index.html.twig #}
{# Отобразит что-то вроде "Привет, Symfony!" #}
<h1>{{ name|greet }}</h1>

Как это работает? Symfony замечает, что ваш класс расширяет AbstractExtension и поэтому автоматически регистрирует его в качестве расширения Twig. Это называется автоконфигурацией и работает для очень многих вещей. Просто создайте класс, а потом расширьте базовый класс (или реализуйте интерфейс). Symfony позаботится обо всём остальном.

Со скоростью ветра: Кешированный контейнер

Увидев, сколько всего Symfony делает автоматически, вы можете подумать: "Разве это не вредит производительности?". На самом деле, нет! Symfony работает со скоростью ветра.

Как это возможно? Система сервисов упавляется очень важным объектом под названием "контейнер". Большинство фреймворков имеют контейнер, но в Symfony он уникален, так как он кеширован. Когда вы загружаете вашу первую страницу, вся информация о сервисе была скомпилирована и сохранена. Это означает, что функции автомонтирования и автоконфигурации не добавляют нагрузки! Это также означает, что вы получаете отличные ошибки: Symfony исследует и валидирует всё, когда строится контейнер.

Теперь вы можете интересоваться, что просиходит, когда вы обновляете файл и кешу нужно построиться заново? Мне нравится ваше мышление! Он достаточно умён, чтобы построиться при следующей загрузке страницы. Но на самом деле это тема следующего раздела.

Разработка против производства: окружения

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

Но что если вы развёртываете в производстве? Нам нужно будет спрятать эти инструменты и оптимизироваться для скорости!

Это решается системой окружений Symfony и их есть три: dev, prod и test. В зависимости от окружения, Symfony загружает разные файлы в каталог config/:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
config/
├─ services.yaml
├─ ...
└─ packages/
    ├─ framework.yaml
    ├─ ...
    ├─ **dev/**
        ├─ monolog.yaml
        └─ ...
    ├─ **prod/**
        └─ monolog.yaml
    └─ **test/**
        ├─ framework.yaml
        └─ ...
└─ routes/
    ├─ annotations.yaml
    └─ **dev/**
        ├─ twig.yaml
        └─ web_profiler.yaml

Это мощная идея: изменяя одну часть конфигурации (окружение), ваше приложение трансформируется из приятного опыта отладки в оптимизированное для скорости.

Ой, а как изменить окружение? Измените переменную окружения APP_ENV с dev на prod:

1
2
3
# .env
- APP_ENV=dev
+ APP_ENV=prod

Но я хочу больше поговорить о переменных окружений дальше. Измените значение обратно на dev: инструменты отладки прекрасны, когда вы работаете локально.

Переменные окружения

Каждое приложение содержит конфигурацию, которая отличается на каждом сервере - вроде информации о соединениях DB или паролей. Как их нужно хранить? В файлах? Или каким-то другим способом?

Symfony следует лучшей практике индустрии, храня конфигурацию, основанную на сервере, в виде переменных окружения. Это означает, что Symfony отлично работает с системами развёртывания Платформы, как Сервиса (PaaS), а также с Docker.

Но установка переменных окружения во время разработки может быть напряжной. Поэтому наше приложение автоматически загружает файл .env , если переменная окружения APP_ENV не установлена в окружении. Ключи в этом файле потом становятся переменными окружения и считываются вашим приложением:

1
2
3
4
5
# .env
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10
###< symfony/framework-bundle ###

Cначала, файл не содержит многого. Но с ростом вашего приложения, вы добавите больше конфигурации по мере необходимости. Но, на самом деле, становится намного интереснее! Представьте, что вашему приложение нужно DB ORM. Давайте установим Doctrine ORM:

1
$ composer require doctrine

Благодаря новому рецепту, установленному Flex, посмотрите на файл .env ещё раз:

1
2
3
4
5
6
7
8
9
###> symfony/framework-bundle ###
  APP_ENV=dev
  APP_SECRET=cc86c7ca937636d5ddf1b754beb22a10
  ###< symfony/framework-bundle ###

+ ###> doctrine/doctrine-bundle ###
+ # ...
+ DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
+ ###< doctrine/doctrine-bundle ###

Новая переменная окружения DATABASE_URL была добавлена автоматически и на неё уже ссылается новый файл конфигурации doctrine.yaml. Объединив переменные окружения и Flex, вы используете лучшую практику индустрии без дополнительных усилий.

Продолжайте!

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

Вот и всё для быстрого тура. От аутентификации, до форм, до кеширования - ещё столько предстоит исследовать. Готовы погрузиться в эти темы? Тогда не ищите, а переходите на официальный Документация Symfony и выберите любой справочник, который хотите.