跳到内容

如何使用单个内核创建多个 Symfony 应用程序

编辑此页

在 Symfony 应用程序中,传入的请求通常由 public/index.php 的前端控制器处理,该控制器实例化 src/Kernel.php 类以创建应用程序内核。此内核加载 bundles、配置并处理请求以生成响应。

当前 Kernel 类的实现作为单个应用程序的便捷默认设置。但是,它也可以管理多个应用程序。虽然 Kernel 通常使用基于各种环境的不同配置运行相同的应用程序,但它可以进行调整以运行具有特定 bundles 和配置的不同应用程序。

以下是使用单个 Kernel 创建多个应用程序的一些常见用例

  • 定义 API 的应用程序可以分为两个部分以提高性能。第一部分服务于常规 Web 应用程序,而第二部分专门响应 API 请求。这种方法需要为第二部分加载更少的 bundles 和启用更少的功能,从而优化性能;
  • 高度敏感的应用程序可以分为两部分以增强安全性。第一部分将仅加载与应用程序的公开部分相对应的路由。第二部分将加载应用程序的其余部分,其访问由 Web 服务器保护;
  • 单体应用程序可以逐步转换为更分布式的架构,例如微服务。这种方法允许大型应用程序的无缝迁移,同时仍然共享通用配置和组件。

将单个应用程序转换为多个应用程序

以下是将单个应用程序转换为支持多个应用程序的新应用程序所需的步骤

  1. 创建一个新的应用程序;
  2. 更新 Kernel 类以支持多个应用程序;
  3. 添加一个新的 APP_ID 环境变量;
  4. 更新前端控制器。

以下示例展示了如何为新的 Symfony 项目的 API 创建一个新的应用程序。

步骤 1) 创建一个新的应用程序

此示例遵循 Shared Kernel 模式:所有应用程序都维护一个隔离的上下文,但如果需要,它们可以共享通用的 bundles、配置和代码。最佳方法将取决于您的具体需求和要求,因此由您决定哪种方法最适合您的项目。

首先,在您的项目根目录下创建一个新的 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

注意

请注意,项目根目录下的 config/src/ 目录将代表 apps/ 目录中所有应用程序之间的共享上下文。因此,您应该仔细考虑哪些是通用的,哪些应该放在特定的应用程序中。

提示

您还可以考虑重命名共享上下文的命名空间,从 AppShared,因为这将使其更容易区分并为该上下文提供更清晰的含义。

由于新的 apps/api/src/ 目录将托管与 API 相关的 PHP 代码,因此您必须更新 composer.json 文件以将其包含在自动加载部分中

1
2
3
4
5
6
7
8
{
    "autoload": {
        "psr-4": {
            "Shared\\": "src/",
            "Api\\": "apps/api/src/"
        }
    }
}

此外,不要忘记运行 composer dump-autoload 以生成自动加载文件。

步骤 2) 更新 Kernel 类以支持多个应用程序

由于将有多个应用程序,因此最好在 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
96
97
98
// src/Kernel.php
namespace Shared;

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 __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';

        // load common bundles, such as the FrameworkBundle, as well as
        // specific bundles required exclusively for the app itself
        foreach (array_merge($sharedBundles, $appBundles) as $class => $envs) {
            if ($envs[$this->environment] ?? $envs['all'] ?? false) {
                yield new $class();
            }
        }
    }

    public function getCacheDir(): string
    {
        // divide cache for each application
        return ($_SERVER['APP_CACHE_DIR'] ?? $this->getProjectDir().'/var/cache').'/'.$this->id.'/'.$this->environment;
    }

    public function getLogDir(): string
    {
        // divide logs for each application
        return ($_SERVER['APP_LOG_DIR'] ?? $this->getProjectDir().'/var/log').'/'.$this->id;
    }

    protected function configureContainer(ContainerConfigurator $container): void
    {
        // load common config files, such as the framework.yaml, as well as
        // specific configs required exclusively for the app itself
        $this->doConfigureContainer($container, $this->getSharedConfigDir());
        $this->doConfigureContainer($container, $this->getAppConfigDir());
    }

    protected function configureRoutes(RoutingConfigurator $routes): void
    {
        // load common routes files, such as the routes/framework.yaml, as well as
        // specific routes required exclusively for the app itself
        $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 文件中以提供默认值,但它通常应该添加到您的 Web 服务器配置中。

1
2
# .env
APP_ID=api

警告

此变量的值必须与 apps/ 中的应用程序目录匹配,因为它在 Kernel 中用于加载特定的应用程序配置。

步骤 4) 更新前端控制器

在最后一步中,更新前端控制器 public/index.phpbin/console,以将 APP_ID 变量的值传递给 Kernel 实例。这将允许 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_ENVAPP_DEBUG 值类似,Kernel 构造函数的第三个参数现在也需要设置应用程序 ID,该 ID 来自外部配置。

对于第二个前端控制器,定义一个新的控制台选项,以允许传递应用程序 ID 以在 CLI 上下文中运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bin/console
use Shared\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;

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
// or
php bin/console cache:clear -iapi

// alternatively
export APP_ID=api
php bin/console cache:clear

您可能需要更新 composer auto-scripts 部分以同时运行多个命令。此示例显示了两个不同应用程序(名为 apiadmin)的命令

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 进行测试!

注意

每个控制台脚本(例如 bin/console -iapibin/console -iadmin)可用的命令可能不同,因为它们取决于为每个应用程序启用的 bundles,这些 bundles 可能不同。

渲染模板

假设您需要创建另一个名为 admin 的应用程序。如果您遵循Symfony 最佳实践,则共享的 Kernel 模板将位于项目根目录的 templates/ 目录中。对于特定于 admin 的模板,您可以创建一个新的目录 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() 的方法,该方法尝试创建负责在测试期间运行应用程序的内核。但是,此方法的当前逻辑不包含新的应用程序 ID 参数,因此您需要更新它

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

注意

此示例使用硬编码的应用程序 ID 值,因为扩展此 ApiTestCase 类的测试将仅关注 api 测试。

现在,在 apps/api/ 应用程序内部创建一个 tests/ 目录。然后,更新 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/
│     └─ ...

此外,您可能需要更新您的 Web 服务器配置,以在不同的域下设置 APP_ID=admin

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