Pull to refresh

Приложение Symfony2 в разрезе

Reading time6 min
Views24K
Наблюдая открыв рот за виртуозным фокусником и его красивыми помощницами, многие, однако, сосредоточенны совсем на другом: как он это делает? как там все устроенно внутри?


Тоже самое происходит и при работе с фреймворками, которые все делают за нас, но хочется знать, как они это делают, и в случае необходимости иметь возможность изменить поведение. К сожалению, документация, какой бы хорошей она не была (а у Symfony 2 она уже неплоха), рассказывает, как использовать всю эту «магию», но не раскрывает всей сути.

Эта статья — попытка разобраться, как происходит инициализация приложения и что же такое «Ядро Symfony2».

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

Краткое описание процесса




Кликабельно

1. Все запросы принимаются Фронт-контроллером (FrontController).
2. Настраивается автозагрузка классов (autoload).
3. Создается ядро в зависимости от окружения.
4. Запускается ядро.
    1. Инициализируется список Бандлов (Bundles).
    2. Создается Контейнер зависимостей (Dependency Injection Container).
        1. Создается контейнер с основными параметрами.
        2. Каждый бандл модифицирует (build) контейнер.
        3. Загружается конфигурация приложения.
        4. Контейнер компилируется.
            1. Обрабатываются расширения.
            2. Ссылки на параметры заменяются реальными значениями.
            3. Контейнер переводится в режим только на чтение (frozen).
    3. Запускаются бандлы.


FrontController


В качестве фронт-контроллера выступают обычные скрипты. Примерами могут служить скрипты из стандартной поставки Symfony2.
  • app/console — доступ к приложению через CLI.
  • web/app.php — доступ к приложению через web.
  • web/app_dev.php — доступ к приложению через web в режиме разработки.

Все эти фронт-контроллеры строятся по одному принципу:
  • настраивается autoload путем подключения скрипта с регистрацией загрузчика классов;
  • иногда используется кэш классов (часто используемые классы, собранные в 1 файл);
  • запускается Ядро;


Ядро (Kernel)


Ядро — это класс, реализующий интерфейс KernelInterface, его задача — инициализация окружения. В основе ядра лежат два основных компонента: Dependency Injection контейнер (теорию можно почерпнуть, например, у Фаулера) и система бандлов. Бандл (bundle) — аналог плагина из Symfony 1.x. Подробнее о бандлах можно прочитать в официальной документации. Конечно, кроме непосредственно интерфейса ядра существует и стандартная абстрактная реализация Kernel, о которой в основном и пойдет речь дальше.
Код инициализации выглядит примерно следующим образом:
   // init bundles
   $this->initializeBundles();

   // init container
   $this->initializeContainer();

   foreach ($this->getBundles() as $bundle) {
       $bundle->setContainer($this->container);
       $bundle->boot();
   }

  • (4.1) initializeBundles — собирает информацию о бандлах (тех самых, что регистрирует разработчик для своего приложения через KernelInterface::registerBundles, см. пример из стандартной поставки);
  • (4.2) initializeContainer — создает DI контейнер и заполняет его данными;
  • (4.3) запускается каждый из бандлов.

Инициализация контейнера


Результатом данного этапа является полностью готовый к работе контейнер, переведенный в режим read-only. Делится этот процесс на 4 подэтапа:
  • Создается контейнер (ContainerBuilder) и туда помещаются стандартные параметры ядра: пути до основных директорий, параметры окружения (environment, debug) и другие.
  • Для каждого бандла запускается build, на этом этапе бандлам предоставляется возможность модификации контейнера
  • Добавляется конфигурация, определенная разработчиком (через registerContainerConfiguration ядра). В стандартной поставке этот метод выглядит очень просто:
        public function registerContainerConfiguration(LoaderInterface $loader)
        {
            $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
        }
    
  • И наконец, контейнер компилируется (compile): подставляются реальные значения параметров; выполняются «проходы компилятора»* (CompilerPass), именно они выполняют модификации, добавленные бандлами (подробнее о них далее); контейнер «замораживается», т.е. переходит в режим только на чтение, после чего любая попытка его изменения вызовет исключение.
*В теории компиляторов есть понятие «прохода», здесь подразумевается примерно то же самое, поэтому CompilerPass перевел именно так.

Модификация контейнера бандлами


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

Compiler Pass


На этапе компиляции контейнер выполняет серию проходов для приведения своего содержимого в конечное состояние. Проходы являются реализацией интерфейса CompilerPassInterface и бывают 6 видов (в порядке исполнения): merge, beforeOptimization, optimization, beforeRemoving, removing, afterRemoving — по умолчанию конфигурация компилятора уже содержит набор проходорв.
  • before*/after* — лишь «хуки» для пользовательских «проходов» и не используются стандартными (добавленными по умолчанию);
  • merge — обрабатывает расширения (extensions) контейнера;
  • optimization — оптимизируют контейнер, примерами могут служить: ResolveInterfaceInjectorsPass (преобразует интерфейсные инъекции), CheckCircularReferencesPass (проверяет циклические ссылки в контейнере)
  • removing — удаляют неиспользуемые сервисы, например, RemoveUnusedDefinitionsPass (удаляет неиспользуемые приватные сервисы)

В бандлах CompilerPass чаще всего используется для обработки тегов в контейнере. Пример из стандартного TwigBundle:
    $definition = $container->getDefinition('twig');
    $calls = $definition->getMethodCalls();
    $definition->setMethodCalls(array());
    foreach ($container->findTaggedServiceIds('twig.extension') as $id => $attributes) {
        $definition->addMethodCall('addExtension', array(new Reference($id)));
    }
    $definition->setMethodCalls(array_merge($definition->getMethodCalls(), $calls));


Расширения контейнера


Контейнер является центральным элементов системы, фактически, в нем сосредоточенна вся функциональность приложения, соответственно, чтобы добавить какую либо функциональность, нужно поместить в контейнер необходимые сервисы и параметры. Для удобства подобных модификаций существуют расширения контейнера (которые обрабатываются специальным проходом типа «merge», упомянутым выше).
Расширения — реализация интерфейса ExtensionInterface, а абстрактный класс Extension уменьшает рутинные действия и добавляет удобные методы. Работают обычно расширения следующим образом:
  • загружаются определения сервисов и параметров по умолчанию из конфига расширения (чаще всего xml файл);
  • обрабатывается конфиг расширения, определенный для приложения, переписывая значения по умолчанию;
  • регистрируются классы для сборки в один файл (тот самый кэш классов в одном файле, упомянутый во фронт-контроллере)

Пример конфигурации расширения (app/config/config.yml):
# Twig Configuration
twig:
    debug: %kernel.debug%
    strict_variables: %kernel.debug%

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

Подключить расширение бандла очень просто, нужно лишь создать класс с соответствующим именем (DependencyInjection\BundleNameExtension) и код метода Extension::build() сам подгрузит расширение (главное не забыть вызвать родительский метод и наследника ;) ).

Запуск бандла


И последний этап — запуск бандлов. На самом деле, большинство бандлов ничего не делают на данном этапе, он предназначен для выполнения действий, которые не могут быть выполнены контейнером. Например, основной бандл Symfony2 (FrameworkBundle) именно на этом этапе загружает кэш классов, уже не раз упомянутый в этой статье.

Заключение


На этом процесс инициализации приложение Symfony2 можно считать завершенным: все подготовительные этапы выполнены, а контейнер содержит всю необходимую функциональность. Остается достать из контейнера нужный сервис и вызвать его методы для выполнения приложения.
Читатель, наверняка, обеспокоится довольно сложным и ресурсоемким процессом инициализации, но все не так страшно. Symfony2 наряду с гибкостью не забывает и о скорости, поэтому «из коробки» предоставляет множество решений по оптимизации. Так, например, весь процесс создания контейнера кэшируется путем создания класса, содержащего все настройки, и при следующем вызове (если это режим отладки) вместо загрузки множества конфигов различных параметров, работы компилятора контейнера и т.п. будет всего лишь загружен сгенерированный класс и сразу создан готовый объект контейнера.

От автора


На этом завершается первое погружение в дебри Symfony2. Если подобный тип статей о Symfony2 заинтересует читателей, то возможно превратить все это в цикл статей с разбором внутренностей фреймворка, отдельных компонентов и популярных бандлов. Кроме того, это моя первая статья, несмотря на долгое прибывание на Хабре. Я больше люблю говорить чем писать, но пытаюсь воспитывать в себе и такой способ изложения своих мыслей. Поэтому буду очень признателен за отзывы непосредственно о подаче материала: насколько сложно воспринимается; на что лучше делать больший акцент, на что меньший; возможно нужно больше иллюстраций и примеров кода; рассчитывать больше на тех, кто знаком с предметной областью, или стараться описать, чтобы было понятно всем и т.д.
Tags:
Hubs:
+72
Comments21

Articles

Change theme settings