跳到内容

使用 MicroKernelTrait 构建您自己的框架

编辑此页

Symfony 应用中包含的默认 Kernel 类使用 MicroKernelTrait 在同一个类中配置程序包、路由和服务容器。

这种微内核方法非常灵活,允许您控制应用程序的结构和功能。

单文件 Symfony 应用

从一个完全空的目录开始,并通过 Composer 安装这些 Symfony 组件

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 本地 Web 服务器

1
$ symfony server:start

然后在浏览器中查看 JSON 响应:https://127.0.0.1:8000/random/10

提示

如果您的内核只定义一个控制器,则可以使用可调用方法

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 以自动将内核本身注册为编译器传递,如专用 编译器传递部分 中所述。如果在使用 MicroKernelTrait 时实现了 ExtensionInterface,则内核将自动注册为扩展。您可以在关于 使用扩展管理配置 的专用部分中了解更多信息。

也可以实现 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、注解和 Web 调试工具栏

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\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
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(): iterable
    {
        yield FrameworkBundle();
        yield TwigBundle();

        if ('dev' === $this->getEnvironment()) {
            yield WebProfilerBundle();
        }
    }

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

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

        // register all classes in /src/ as service
        $container->services()
            ->load('App\\', __DIR__.'/*')
            ->autowire()
            ->autoconfigure()
        ;

        // configure WebProfilerBundle only if the bundle is enabled
        if (isset($this->bundles['WebProfilerBundle'])) {
            $container->extension('web_profiler', [
                'toolbar' => true,
                'intercept_redirects' => false,
            ]);
        }
    }

    protected function configureRoutes(RoutingConfigurator $routes): void
    {
        // import the WebProfilerRoutes, only if the bundle is enabled
        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');
        }

        // load the routes defined as PHP attributes
        // (use 'annotation' as the second argument if you define routes as annotations)
        $routes->import(__DIR__.'/Controller/', 'attribute');
    }

    // optional, to use the standard Symfony cache directory
    public function getCacheDir(): string
    {
        return __DIR__.'/../var/cache/'.$this->getEnvironment();
    }

    // optional, to use the standard Symfony logs directory
    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);
        }
    }
}

与之前的内核不同,这会加载一个外部 config/framework.yaml 文件,因为配置开始变得更大

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);

就是这样!这个 /random/10 URL 将会工作,Twig 将会渲染,您甚至会在底部看到 Web 调试工具栏。最终结构看起来像这样

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 本地 Web 服务器

1
$ symfony server:start

然后访问您浏览器中的页面:https://127.0.0.1:8000/random/10

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可证获得许可。
目录
    版本