Побудова власного фреймворку з MicroKernelTrait

Дата оновлення перекладу 2025-01-11

Побудова власного фреймворку з MicroKernelTrait

Клас Kernel за замовчуванням додоаний у додатки Symfony використовує MicroKernelTrait, щоб конфігурувати пакети, маршрути та сервіс-контейнер в одному класі.

Цей підхід мікро-ядра настільки гнучкий, що дозволяє вам контролювати структуру вашого додатку та функції достатньо легко.

Додаток Symfony з одного файлу

Почніть з абсолютно пороженього каталогу. Та встановіть ці компоненти Symfony через Composer:

1
$ composer require symfony/framework-bundle symfony/runtime

Далі, створіть файл index.php, що визначає клас ядра та виконує його:

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
// index.php
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Attribute\Route;

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    protected function configureContainer(ContainerConfigurator $container): void
    {
        // PHP equivalent of config/packages/framework.yaml
        $container->extension('framework', [
            'secret' => 'S0ME_SECRET'
        ]);
    }

    #[Route('/random/{limit}', name: 'random_number')]
    public function randomNumber(int $limit): JsonResponse
    {
        return new JsonResponse([
            'number' => random_int(0, $limit),
        ]);
    }
}

return static function (array $context) {
    return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

Ось і все! Щоб протестувати, запустіть Локальний веб-сервер Symfony:

1
$ symfony server:start

Потім подивіться на JSON-відповідь у вашому браузері: http://localhost:8000/random/10

Tip

Якщо у ваше ядро визначає лише один контролер, ви можете використати метод, що викликається:

1
2
3
4
5
6
7
8
9
10
11
12
class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // ...

    #[Route('/random/{limit}', name: 'random_number')]
    public function __invoke(int $limit): JsonResponse
    {
        // ...
    }
}

Методи "мікро" ядра

Коли ви використовуєте MicroKernelTrait, ваше ядро вимагає рівно три методи, що визначають ваші пакети, сервіси та маршрути:

registerBundles()

Це те ж саме, що і registerBundles(), який ви бачите у звичайному ядрі. За замовчуванням мікроядро реєструє лише FrameworkBundle. Якщо вам потрібно зареєструвати більше пакетів, перевизначте цей метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
// ...

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    // ...

    public function registerBundles(): array
    {
        yield new FrameworkBundle();
        yield new TwigBundle();
    }
}
configureContainer(ContainerConfigurator $container)
Цей метод будує та конфігурує контейнер. На практиці, ви використовуватимете extension(), щоб сконфігурувати різні пакети (це еквівалент того, що ви бачите у нормальному файлі config/packages/*). Ви можете також зареєструвати сервіси напряму в PHP або завантажити зовнішні файли конфігурації (продемонстровано нижче).
configureRoutes(RoutingConfigurator $routes)

У цьому методі ви можете використовувати об'єкт RoutingConfigurator для визначення маршрутів у вашому додатку і пов'язати їх з контролерами, визначеними у цьому самому файлі.

Однак, зручніше визначати маршрути контролерів за допомогою атрибутів PHP, як показано вище. Ось чому цей метод зазвичай використовується тільки для завантаження зовнішніх файлів маршрутизації (наприклад, з пакетів), як показано нижче.

Додавання інтерфейсів до "мікро" ядра

При використанні MicroKernelTrait, ви також можете реалізувати CompilerPassInterface, щоб автоматично реєструвати саме ядро як пропуск компілятора, як пояснюється у відповідному розділі про передачу компілятора . Якщо ExtensionInterface реалізовано при використанні MicroKernelTrait, то ядро буде автоматично зареєстровано як розширення. Більш детально про це можна дізнатися у спеціальному розділі про керування конфігурацією з розширеннями .

Також можливо реалізувати EventSubscriberInterface, щоб обробляти події напряму з ядра, і знову він буде зареєстрований автоматично:

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
// ...
use App\Exception\Danger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class Kernel extends BaseKernel implements EventSubscriberInterface
{
    use MicroKernelTrait;

    // ...

    public function onKernelException(ExceptionEvent $event): void
    {
        if ($event->getThrowable() instanceof Danger) {
            $event->setResponse(new Response('It\'s dangerous to go alone. Take this ⚔'));
        }
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::EXCEPTION => 'onKernelException',
        ];
    }
}

Просунутий приклад: Twig, анотації та панель інструментів веб-налагодження

Ціль MicroKernelTrait не в тому, щоб мати додаток з одного файлу. Вона в тому, щоб надати вам мможливість обирати ваші пакети та структуру.

Спочатку ви напевно захочете помістити ваші PHP-класи в каталог src/. Сконфігуруйте ваш файл composer.json так, щоб він завантажував звідти:

1
2
3
4
5
6
7
8
9
10
{
    "require": {
        "...": "..."
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

Потім, виконайте composer dump-autoload, щоб скинути вашу нову конфігурацію автозавантаження.

Тепер, уявіть, що ви хочете використати Twig і завантажувати маршрути через анотації. Замість того, щоб розміщувати все у index.php, створіть новий src/Kernel.php, щоб утримувати ядро. Тепер він виглядає так:

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
// src/Kernel.php
namespace App;

use App\DependencyInjection\AppExtension;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    public function registerBundles(): array
    {
        $bundles = [
            new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new \Symfony\Bundle\TwigBundle\TwigBundle(),
        ];

        if ('dev' === $this->getEnvironment()) {
            $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        }

        return $bundles;
    }

    protected function build(ContainerBuilder $containerBuilder): void
    {
        $containerBuilder->registerExtension(new AppExtension());
    }

    protected function configureContainer(ContainerConfigurator $container): void
    {
        $container->import(__DIR__.'/../config/framework.yaml');

        // зареєструвати всі класи у /src/ як сервіси
        $container->services()
            ->load('App\\', __DIR__.'/*')
            ->autowire()
            ->autoconfigure()
        ;

        // сконфігурувати WebProfilerBundle лише якщо пакет підключено
        if (isset($this->bundles['WebProfilerBundle'])) {
            $container->extension('web_profiler', [
                'toolbar' => true,
                'intercept_redirects' => false,
            ]);
        }
    }

    protected function configureRoutes(RoutingConfigurator $routes): void
    {
        // імпортувати WebProfilerRoutes, лише якщо пакет підключено
        if (isset($this->bundles['WebProfilerBundle'])) {
            $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt');
            $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler');
        }

        // завантажити маршрути, визначені як PHP-атрибути
        // (використайте 'annotation' в якості другого аргументу, якщо ви визначаєте маршрути як анотації)
        $routes->import(__DIR__.'/Controller/', 'attribute');
    }

    // опціонально, щоб використати стандартний каталог кеша Symfony
    public function getCacheDir(): string
    {
        return __DIR__.'/../var/cache/'.$this->getEnvironment();
    }

    // опціонально, щоб використати стандартний каталог логів Symfony
    public function getLogDir(): string
    {
        return __DIR__.'/../var/log';
    }
}

Перед тим, як продовжувати, виконайте цю команду, щоб додати підтримку для нових залежностей:

1
$ composer require symfony/yaml symfony/twig-bundle symfony/web-profiler-bundle

Далі, створіть новий клас розширень, який визначає конфігурацію вашого додатку та додайте сервіс, умовно заснований на значенні foo:

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
// src/DependencyInjection/AppExtension.php
namespace App\DependencyInjection;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\AbstractExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

class AppExtension extends AbstractExtension
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->booleanNode('foo')->defaultTrue()->end()
            ->end();
    }

    public function loadExtension(array $config, ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void
    {
        if ($config['foo']) {
            $containerBuilder->register('foo_service', \stdClass::class);
        }
    }
}

На відміну від попереднього ядра, це завантажує зовнішній файл app/config/config.yml, так як конфігурація стає більшою:

1
2
3
4
# config/framework.yaml
framework:
    secret: S0ME_SECRET
    profiler: { only_exceptions: false }

Також завантажує маршрути атрибутів з каталогу src/Controller/, який містить в собі один файл:

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

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

class MicroController extends AbstractController
{
    #[Route('/random/{limit}')]
    public function randomNumber(int $limit): Response
    {
        $number = random_int(0, $limit);

        return $this->render('micro/random.html.twig', [
            'number' => $number,
        ]);
    }
}

Файли шаблонів повинні знаходитись у каталозі templates/ у корені вашого проекту. Цей шаблон знаходиться за адресою templates/micro/random.html.twig:

1
2
3
4
5
6
7
8
9
10
<!-- templates/micro/random.html.twig -->
<!DOCTYPE html>
<html>
    <head>
        <title>Random action</title>
    </head>
    <body>
        <p>{{ number }}</p>
    </body>
</html>

Нарешті, вам потрібен фронт-контролер для завантаження та запуску додатку. Створіть public/index.php:

1
2
3
4
5
6
7
8
9
10
11
// public/index.php
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;

require __DIR__.'/../vendor/autoload.php';

$kernel = new Kernel('dev', true);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Ось і все! Цей URL /random/10 працюватиме, Twig відображатиме, і ви навіть побачите знизу панель інструментів веб-налагодження. Підсумкова структура виглядає так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
your-project/
├─ config/
│  └─ framework.yaml
├─ public/
|  └─ index.php
├─ src/
|  ├─ Kernel.php
|  ├─ Controller
|  |  └─ MicroController.php
│  └─ Resources
|     └─ views
|        └─ micro
|           └─ random.html.twig
├─ var/
|  ├─ cache/
│  └─ log/
├─ vendor/
│  └─ ...
├─ composer.json
└─ composer.lock

Як і раніше, ви можете використати вбудований PHP-сервер:

1
$ symfony server:start

Потім відвідайте сторінку у вашому браузері: http://localhost:8000/random/10