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

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

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

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

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

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

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

1
2
3
$ composer require symfony/config symfony/http-kernel \
  symfony/http-foundation symfony/routing \
  symfony/dependency-injection symfony/framework-bundle

Далі, створіть файл 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
35
36
37
38
39
40
41
42
43
// 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\Annotation\Route;

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

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    public function registerBundles(): array
    {
        return [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        ];
    }

    protected function configureContainer(ContainerConfigurator $container): void
    {
        // PHP-еквівалент 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),
        ]);
    }
}

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

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

1
$ symfony server:start

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

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

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

registerBundles()
Той же registerBundles(), який ви бачите у звичайному ядрі.
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,
        ]);
    }
}

Файли шаблонів повинні жити у каталозі Resources/views/ того ж каталогу, в якому знаходиться ваше ядро. Так як Kernel живе в src/, то цей шаблон знаходиться в src/Resources/views/micro/random.html.twig:

1
2
3
4
5
6
7
8
9
10
<!-- src/Resources/views/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