Как создать несколько приложений Symfony с одним ядром
Дата обновления перевода 2024-07-16
Как создать несколько приложений Symfony с одним ядром
В приложениях Symfony входящие запросы обычно обрабатываются фронт-контроллером по
адресу public/index.php
, который инстанцирует класс rc/Kernel.php
для создания
ядра программы. Это ядро загружает пакеты и конфигурацию, и обрабатывает запрос для
генерации ответа.
Текущая реализация класса Kernel служит удобным вариантом по умолчанию для одного приложения. Однако он также может управлять несколькими приложениями. В то время как ядро обычно запускает одно и то же приложение с различными конфигурациями на основе различных окружений , его можно адаптировать для запуска различных приложений с определёнными пакетами и конфигурацией.
Вот некоторые из распространённых случаев использования для создания нескольких приложений с одним ядром:
- Приложение, которое определяет API, можно разделить на два сегмента для улучшения производительности. Первый сегмент обслуживает обычное веб-приложение, в то время как второй сегмент отвечает исключительно на запросы API. Такой подход требует загрузки меньшего количества пакетов и включения меньшего количества функций для второй части, таким образом оптимизируя производительность;
- Высокочувствительное приложение можно разделить на две части для повышения безопасности. Первая часть будет загружать только маршруты, соответствующие общедоступным разделам приложения. Вторая часть будет загружать остальную часть приложения, а доступ к ней будет защищён веб-сервером;
- Монолитное приложение можно постепенно трансформировать в более распределённую архитектуру, например, микросервисы. Такой подход позволяет осуществлять беспрепятственную миграцию большого приложения, сохраняя при этом общие конфигурации и компоненты.
Преобразование одного приложения в несколько приложений
Вот шаги, необходимые для преобразования одного приложения в новое, которое будет будет поддерживать несколько приложений:
- Создать новое приложение;
- Обновить класс Kernel для поддержки нескольких приложений;
- Добавить новую переменную окружения
APP_ID
; - Обновить фронт-контроллеры.
Следующий пример показывает как создать новое приложение для API нового проекта Symfony.
Шаг 1) Создать новое приложение
Этот пример следует паттерну Shared Kernel: все приложения поддерживают изолированный контекст, но они могут использовать общие пакеты, конфигурацию и код, по желанию. Оптимальный подход будет зависеть от ваших конкретных потребностей и требований, поэтому только вам решать, что лучше всего подходит для вашего проекта.
Во-первых, создайте новый каталог apps
в корне вашего проекта, который будет содержать
все необходимые приложения. Каждое приложение будет иметь упрощённую структуру каталогов, подобную
той, что описана в Лучших практиках Symfony:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
your-project/
├─ apps/
│ └─ api/
│ ├─ config/
│ │ ├─ bundles.php
│ │ ├─ routes.yaml
│ │ └─ services.yaml
│ └─ src/
├─ bin/
│ └─ console
├─ config/
├─ public/
│ └─ index.php
├─ src/
│ └─ Kernel.php
Note
Заметьте, что каталоги config/
и rc/
в корне проекта будут представлять
общий контекст для всех приложений в каталоге apps/
. Поэтому вам следует тщательно
продумать, что является общим, а что должно быть размещено в конкретном приложении.
Tip
Вы также можете рассмотреть возможность переименования пространства имён для общего
контекста с App
на Shared
, поскольку это облегчит его различение и даст
более чёткое значение этого контекста.
Поскольку в новом каталоге apps/api/src/
будет размещён PHP-код, связанный с
API, вам следует обновить файл composer.json
, чтобы добавить его в раздел автозагрузки:
1 2 3 4 5 6 7 8
{
"autoload": {
"psr-4": {
"Shared\\": "src/",
"Api\\": "apps/api/src/"
}
}
}
Дополнительно, не забудьте запустить composer dump-autoload
, чтобы сгенерировать файлы
автозагрузки.
Шаг 2) Обновить класс Kernel для поддержки нескольких приложений
Поскольку приложений будет несколько, лучше добавить новое свойство string $id
в ядро для идентификации загружаемого приложения. Это свойство также позволит вам
вам разделить кеш, логи и файлы конфигурации, чтобы избежать коллизий с другими приложениями.
Кроме того, это способствует оптимизации производительности, поскольку каждое приложение будет
загружать только необходимые источники:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
// src/Kernel.php
namespace Shared;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function __construct(string $environment, bool $debug, private string $id)
{
parent::__construct($environment, $debug);
}
public function getSharedConfigDir(): string
{
return $this->getProjectDir().'/config';
}
public function getAppConfigDir(): string
{
return $this->getProjectDir().'/apps/'.$this->id.'/config';
}
public function registerBundles(): iterable
{
$sharedBundles = require $this->getSharedConfigDir().'/bundles.php';
$appBundles = require $this->getAppConfigDir().'/bundles.php';
// загрузить общие пакеты, такие как FrameworkBundle, а также конкретные
// пакеты, необходимые только для самого приложения
foreach (array_merge($sharedBundles, $appBundles) as $class => $envs) {
if ($envs[$this->environment] ?? $envs['all'] ?? false) {
yield new $class();
}
}
}
public function getCacheDir(): string
{
// разделить кеш для каждого приложения
return ($_SERVER['APP_CACHE_DIR'] ?? $this->getProjectDir().'/var/cache').'/'.$this->id.'/'.$this->environment;
}
public function getLogDir(): string
{
// разделить логи для каждого приложения
return ($_SERVER['APP_LOG_DIR'] ?? $this->getProjectDir().'/var/log').'/'.$this->id;
}
protected function configureContainer(ContainerConfigurator $container): void
{
// загрузить общие файлы конфигурации, такие как framework.yaml, а также
// конкретные конфигурации, необходимые только для самого приложения
$this->doConfigureContainer($container, $this->getSharedConfigDir());
$this->doConfigureContainer($container, $this->getAppConfigDir());
}
protected function configureRoutes(RoutingConfigurator $routes): void
{
// загрузить общие файлы маршрутов, такие как routes/framework.yaml, а также
// конкретные маршруты, необходимые только для самого приложения
$this->doConfigureRoutes($routes, $this->getSharedConfigDir());
$this->doConfigureRoutes($routes, $this->getAppConfigDir());
}
private function doConfigureContainer(ContainerConfigurator $container, string $configDir): void
{
$container->import($configDir.'/{packages}/*.{php,yaml}');
$container->import($configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}');
if (is_file($configDir.'/services.yaml')) {
$container->import($configDir.'/services.yaml');
$container->import($configDir.'/{services}_'.$this->environment.'.yaml');
} else {
$container->import($configDir.'/{services}.php');
}
}
private function doConfigureRoutes(RoutingConfigurator $routes, string $configDir): void
{
$routes->import($configDir.'/{routes}/'.$this->environment.'/*.{php,yaml}');
$routes->import($configDir.'/{routes}/*.{php,yaml}');
if (is_file($configDir.'/routes.yaml')) {
$routes->import($configDir.'/routes.yaml');
} else {
$routes->import($configDir.'/{routes}.php');
}
if (false !== ($fileName = (new \ReflectionObject($this))->getFileName())) {
$routes->import($fileName, 'attribute');
}
}
}
В этом примере повторно использована реализация по умолчанию для импорта конфигурации и маршрутов на основе заданного каталога конфигурации. Как было показано ранее, этот подход импортирует как общие, так и специфические для приложения источники.
Шаг 3) Добавить новую переменную окружения APP_ID
Затем определите новую переменную окружения, которая идентифицирует текущее приложение.
Эту новую переменную можно добавить в файл .env
, чтобы предоставить значение по умолчанию,
но обычно её следует добавлять в конфигурацию вашего веб-сервера.
1 2
# .env
APP_ID=api
Caution
Значение этой переменной должно соответствовать каталогу приложения в пределах apps/
,
поскольку он используется в ядре для загрузки конфигурации конкретного приложения.
Шаг 4) Обновить фронт-контроллеры
На этом последнем шаге обновите внешние контроллеры public/index.php
и
bin/console
, чтобы передать значение переменной APP_ID
экземпляру Kernel.
Это позволит ядру загрузить и запустить указанное приложение:
1 2 3 4 5 6 7
// public/index.php
use Shared\Kernel;
// ...
return function (array $context): Kernel {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $context['APP_ID']);
};
Подобно конфигурации необходимых значений APP_ENV
и APP_DEBUG
, третий аргумент
конструктора Kernel теперь также необходим для установки идентификатора приложения, который
получается из внешней конфигурации.
Для второго фронт-контроллера определите новую опцию консоли, которая позволит передавать идентификатор приложения для запуска в контексте CLI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// bin/console
use Shared\Kernel;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
return function (InputInterface $input, array $context): Application {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $input->getParameterOption(['--id', '-i'], $context['APP_ID']));
$application = new Application($kernel);
$application->getDefinition()
->addOption(new InputOption('--id', '-i', InputOption::VALUE_REQUIRED, 'The App ID'))
;
return $application;
};
Это всё!
Выполнение команд
Скрипт bin/console
, который используется для запуска команд Symfony, всегда использует
класс Kernel
для построения приложения и загрузки команд. Если вам нужно запускать консольные команды
для конкретного приложения, вы можете указать опцию --id
вместе с соответствующим значением
идентификатора:
1 2 3 4 5 6 7
php bin/console cache:clear --id=api
// или
php bin/console cache:clear -iapi
// как вариант
export APP_ID=api
php bin/console cache:clear
Вы можете захотеть обновить раздел автоскриптов композитора, чтобы запускать несколько
команд одновременно. В этом примере показаны команды двух разных приложений с именами
api
и admin
:
1 2 3 4 5 6 7 8 9 10
{
"scripts": {
"auto-scripts": {
"cache:clear -iapi": "symfony-cmd",
"cache:clear -iadmin": "symfony-cmd",
"assets:install %PUBLIC_DIR% -iapi": "symfony-cmd",
"assets:install %PUBLIC_DIR% -iadmin --no-cleanup": "symfony-cmd"
}
}
}
Затем запустите composer auto-scripts
, чтобы протестировать это!
Note
Команды, доступные для каждого консольного скрипта (например, bin/console -iapi
и
bin/console -iadmin
), могут отличаться, поскольку они зависят от пакетов,
включённых для каждого приложения, которые могут быть разными.
Отображение шаблонов
Представьте, что вам нужно создать ещё одно приложение под названием admin
. Если вы
придерживаетесь Лучших практик Symfony, то общие шаблоны
ядра будут расположены в каталоге templates/
в корне проекта. Для шаблонов,
предназначенных для администратора, вы можете создать новый каталог apps/admin/templates/
,
который вам нужно будет вручную сконфигурировать в приложении Admin:
1 2 3 4
# apps/admin/config/packages/twig.yaml
twig:
paths:
'%kernel.project_dir%/apps/admin/templates': Admin
Затем используйте это пространство имён Twig, чтобы сослаться на любой шаблон только
в пределах приложения Admin, например, @Admin/form/fields.html.twig
.
Выполнение тестов
В приложениях Symfony функциональные тесты обычно расширяются из класса
WebTestCase по умолчанию.
В его родительском классе KernelTestCase
, есть метод под названием
createKernel()
, который пытается создать ядро, ответственное за запуск
приложения во время тестов. Однако, текущая логика этого метода не включает аргумент
нового идентификатора приложения, поэтому ее нужно обновить:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// apps/api/tests/ApiTestCase.php
namespace Api\Tests;
use Shared\Kernel;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpKernel\KernelInterface;
class ApiTestCase extends WebTestCase
{
protected static function createKernel(array $options = []): KernelInterface
{
$env = $options['environment'] ?? $_ENV['APP_ENV'] ?? $_SERVER['APP_ENV'] ?? 'test';
$debug = $options['debug'] ?? (bool) ($_ENV['APP_DEBUG'] ?? $_SERVER['APP_DEBUG'] ?? true);
return new Kernel($env, $debug, 'api');
}
}
Note
Этот пример использует жёстко закодированное значение идентификатора приложения, так как
тесты, расширяющие этот класс ApiTestCase
, будут фокусироваться исключительно на
тестах api
.
Теперь создайте каталог tests/
внутри приложения apps/api/
. Затем
обновите файл composer.json
и конфигурацию phpunit.xml
, чтобы сообщить
о его существовании:
1 2 3 4 5 6 7 8
{
"autoload-dev": {
"psr-4": {
"Shared\\Tests\\": "tests/",
"Api\\Tests\\": "apps/api/tests/"
}
}
}
Не забудьте запустить composer dump-autoload
, чтоб сгенерировать файлы автозагрузки.
А вот и обновление, необходимое для файла phpunit.xml
:
1 2 3 4 5 6 7 8
<testsuites>
<testsuite name="shared">
<directory>tests</directory>
</testsuite>
<testsuite name="api">
<directory>apps/api/tests</directory>
</testsuite>
</testsuites>
Добавление новых приложений
Теперь вы можете начать добавлять другие приложения при необходимости, например,
приложение admin
для управления конфигурацией и разрешениями проекта. Для этого
вам нужно будет повторить только шаг №1:
1 2 3 4 5 6 7 8 9 10
your-project/
├─ apps/
│ ├─ admin/
│ │ ├─ config/
│ │ │ ├─ bundles.php
│ │ │ ├─ routes.yaml
│ │ │ └─ services.yaml
│ │ └─ src/
│ └─ api/
│ └─ ...
Дополнительно вам может потребоваться обновить конфигурацию вашего веб-сервера, чтобы установить
APP_ID=admin
под другим доменом.