Построение собственного фреймворка с MicroKernelTrait

Дата обновления перевода 2024-07-16

Построение собственного фреймворка с 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\Attribute\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)
Этот метод строит и конфигурирует контейнер. На практике, вы будете использовать loadFromExtension, чтобы сконфигурировать разные пакеты (эквивалент того, что вы видите в обычном файле config/packages/*). Вы можете также зарегистрировать сервисы напрямую в PHP или загрузить внешние файлы конфигурации (показано ниже).
configureRoutes(RouteCollectionBuilder $routes)
Ваша задача в этом методе - добавить маршруты в приложение. RouteCollectionBuilder имеет методы, которые делают добавление маршрутов в 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
78
// src/Kernel.php
namespace App;

use App\DependencyInjection\AppExtension;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
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\Attribute\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
your-project/
├─ config/
│  └─ framework.yaml
├─ public/
|  └─ index.php
├─ src/
|  ├─ Controller
|  |  └─ MicroController.php
|  └─ Kernel.php
├─ templates/
|  └─ micro/
|     └─ random.html.twig
├─ var/
|  ├─ cache/
│  └─ log/
├─ vendor/
│  └─ ...
├─ composer.json
└─ composer.lock

Как и раньше, вы можете использовать локальный веб-сервер Symfony:

1
$ symfony server:start

Просмотрите страницу в браузере: http://localhost:8000/random/10