Як створити декілька додатків Symfony з одним ядром

Дата оновлення перекладу 2024-05-09

Як створити декілька додатків Symfony з одним ядром

У додатках Symfony вхідні запити зазвичай обробляються фронт-контролером за адресою public/index.php, який інстанціює клас rc/Kernel.php для створення ядра програми. Це ядро завантажує пакети та конфігурацію і обробляє запит для генерації відповіді.

Поточна реалізація класу Kernel слугує зручним варіантом за замовчуванням для одного додатку. Однак вона також може керувати кількома додатками. У той час як ядро зазвичай запускає один і той самий додаток з різними конфігураціями на основі різних середовищ , його можна адаптувати для запуску різних додатків з певними пакетами та конфігурацією.

Ось деякі з поширених випадків використання для створення декількох додатків з одним ядром:

  • Додаток, який визначає API, можна розділити на два сегменти для покращення продуктивності. Перший сегмент обслуговує звичайний веб-додаток, тоді як другий сегмент відповідає виключно на запити API. Такий підхід вимагає завантаження меншої кількості пакетів і включення меншої кількості функцій для другої частини, таким чином оптимізуючи продуктивність;
  • Високочутливий додаток можна розділити на дві частини для покращення безпеки. Перша частина завантажуватиме лише маршрути, що відповідають загальнодоступним розділам додатку. Друга частина завантажуватиме решту додатку, а доступ до неї буде захищено веб-сервером;
  • Монолітний додаток можна поступово трансформувати в більш розподілену архітектуру, наприклад, мікросервіси. Такий підхід дозволяє здійснювати безперешкодну міграцію великого додатку, зберігаючи при цьому спільні конфігурації та компоненти.

Перетворення одного додатку на декілька додатків

Ось кроки, необхідні для перетворення одного додатку на новий, який підтримуватиме кілька додатків:

  1. Створити новий додаток;
  2. Оновити клас Kernel для підтримки декількох додатків;
  3. Додати нову змінну середовища APP_ID;
  4. Оновити фронт-контролери.

Наступний приклад показує як створити новий додаток для 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
// src/Kernel.php
namespace Shared;

// ...

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
// bin/console
use Shared\Kernel;
// ...

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 під іншим доменом.