使用 Symfony 4 每个项目存储库有多个应用程序

More than one application per project repository with Symfony 4

我有三个旧应用程序(运行 在 Symfony 2 上),每个应用程序都是在单独的 git 存储库中开发并在各自的虚拟主机中配置的:

  1. company.com公司网站。
  2. admin.company.com 网站管理。
  3. api.company.comAPI公司服务。

尽管如此,他们共享同一个数据库。所以我们决定(公司)将所有这些统一到一个应用程序中,采用 Symfony 4 结构和方法,主要是为了删除大量重复数据并改善其维护。

现在,我正在按照计划将所有内容集成到一个 application/repository 中,但我开始处理一些性能和结构问题:

我想保留早期的虚拟主机并只加载每个域所需的捆绑包和配置:

  1. company.com 仅为公司网站加载包、路由和配置(SwiftmailerBundle,...)
  2. admin.company.com 仅为网站管理加载包、路由和配置(SecurityBundleSonataAdminBundle、...)
  3. api.company.com 仅加载包、路由和配置以提供快速 API 公司服务(SecurityBundleFOSRestBundleNelmioApiDocBundle、.. .)

这是我目前所做的:

// public/index.php

// ...

$request = Request::createFromGlobals();
$kernel = new Kernel(getenv('APP_ENV'), getenv('APP_DEBUG'));

// new method implemented in my src/kernel.php
$kernel->setHost($request->server->get('HTTP_HOST'));

$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

我已经在 Kernel::registerBundles() 方法中检查了当前的主机前缀,并且我只加载了需要的包,但是 bin/console 文件仍然存在问题(它不能像 HTTP_HOST 未为 CLI 定义变量)我想为每个 "sub-app" 等清除缓存。

我一直在研究这个主题,但到目前为止我找不到任何对我的场景有帮助的东西 (Symfony 4)。

是否可以在一个项目存储库下独立地拥有多个应用程序 运行(如单个应用程序)但共享一些配置?实现它的最佳方法是什么?

提前致谢。

您可以创建新环境,例如:adminwebsiteapi。然后通过 apache/nginx 提供环境变量 SYMFONY_ENV,您将能够 运行 专用应用程序并仍然使用子域 company.comadmin.company.comapi.company.com .此外,您将能够轻松地仅加载所需的路由。

根据您要基于此方法创建的应用程序的数量,您可以添加条件以在 AppKernel class 中按项目加载指定的包,或者为每个创建单独的 classes项目。

你也应该看看这篇文章https://jolicode.com/blog/multiple-applications-with-symfony2

multiple kernels 方法可能是解决此类项目的一个不错的选择,但现在考虑使用环境变量、结构和内核实现的 Symfony 4 方法,它可以得到改进。

基于名称的虚拟内核

术语"Virtual Kernel"指的是在单个项目存储库上运行安装多个应用程序(例如api.example.comadmin.example.com)的做法。虚拟内核是 "name-based",这意味着您在每个应用程序上有多个内核名称 运行ning。他们 运行 在同一个物理项目存储库上的事实对最终用户来说并不明显。

简而言之,每个内核名称对应一个应用程序。

基于应用程序的配置

首先,您需要为 configsrcvar 目录复制一个应用程序的结构,并为共享包和配置保留根结构。它应该是这样的:

├── config/
│   ├── admin/
│   │   ├── packages/
│   │   ├── bundles.php
│   │   ├── routes.yaml
│   │   ├── security.yaml
│   │   └── services.yaml
│   ├── api/
│   ├── site/
│   ├── packages/
│   ├── bundles.php
├── src/
│   ├── Admin/
│   ├── Api/
│   ├── Site/
│   └── VirtualKernel.php
├── var/
│   ├── cache/
│   │   ├── admin/
│   │   │   └── dev/
│   │   │   └── prod/
│   │   ├── api/
│   │   └── site/
│   └── log/

接下来,利用Kernel::$name 属性你可以用专用的项目文件(var/cache/<name>/<env>/*)突出运行的应用:

  • <name><Env>DebugProjectContainer*
  • <name><Env>DebugProjectContainerUrlGenerator*
  • <name><Env>DebugProjectContainerUrlMatcher*

这将是性能的关键,因为每个应用程序都有自己的 DI 容器、路由和配置文件。下面是支持前面结构的 VirtualKernel class 的完整示例:

src/VirtualKernel.php

// WITHOUT NAMESPACE!

use Symfony\Component\HttpKernel\Kernel;

class VirtualKernel extends Kernel
{
    use MicroKernelTrait;

    private const CONFIG_EXTS = '.{php,xml,yaml,yml}';

    public function __construct($environment, $debug, $name)
    {
        $this->name = $name;

        parent::__construct($environment, $debug);
    }

    public function getCacheDir(): string
    {
        return $this->getProjectDir().'/var/cache/'.$this->name.'/'.$this->environment;
    }

    public function getLogDir(): string
    {
        return $this->getProjectDir().'/var/log/'.$this->name;
    }

    public function serialize()
    {
        return serialize(array($this->environment, $this->debug, $this->name));
    }

    public function unserialize($data)
    {
        [$environment, $debug, $name] = unserialize($data, array('allowed_classes' => false));

        $this->__construct($environment, $debug, $name);
    }

    public function registerBundles(): iterable
    {
        $commonBundles = require $this->getProjectDir().'/config/bundles.php';
        $kernelBundles = require $this->getProjectDir().'/config/'.$this->name.'/bundles.php';

        foreach (array_merge($commonBundles, $kernelBundles) as $class => $envs) {
            if (isset($envs['all']) || isset($envs[$this->environment])) {
                yield new $class();
            }
        }
    }

    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
    {
        $container->setParameter('container.dumper.inline_class_loader', true);

        $this->doConfigureContainer($container, $loader);
        $this->doConfigureContainer($container, $loader, $this->name);
    }

    protected function configureRoutes(RouteCollectionBuilder $routes): void
    {
        $this->doConfigureRoutes($routes);
        $this->doConfigureRoutes($routes, $this->name);
    }

    private function doConfigureContainer(ContainerBuilder $container, LoaderInterface $loader, string $name = null): void
    {
        $confDir = $this->getProjectDir().'/config/'.$name;
        if (is_dir($confDir.'/packages/')) {
            $loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob');
        }
        if (is_dir($confDir.'/packages/'.$this->environment)) {
            $loader->load($confDir.'/packages/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
        }
        $loader->load($confDir.'/services'.self::CONFIG_EXTS, 'glob');
        if (is_dir($confDir.'/'.$this->environment)) {
            $loader->load($confDir.'/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob');
        }
    }

    private function doConfigureRoutes(RouteCollectionBuilder $routes, string $name = null): void
    {
        $confDir = $this->getProjectDir().'/config/'.$name;
        if (is_dir($confDir.'/routes/')) {
            $routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob');
        }
        if (is_dir($confDir.'/routes/'.$this->environment)) {
            $routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob');
        }
        $routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob');
    }
}

现在您的 \VirtualKernel class 需要一个额外的参数 (name) 来定义要加载的应用程序。为了让自动加载器找到您的新 \VirtualKernel class,确保将其添加到 composer.json 自动加载部分:

"autoload": {
    "classmap": [
        "src/VirtualKernel.php"
    ],
    "psr-4": {
        "Admin\": "src/Admin/",
        "Api\": "src/Api/",
        "Site\": "src/Site/"
    }
},

然后,运行 composer dump-autoload 转储新的自动加载配置。

为所有应用程序保留一个入口点

├── public/
│   └── index.php

遵循与 Symfony 4 相同的 filosofy,而环境变量决定应该使用哪个开发环境和调试模式 运行 您的应用程序,您可以添加一个新的 APP_NAME 环境变量来设置要执行的应用程序:

public/index.php

// ...

$kernel = new \VirtualKernel(getenv('APP_ENV'), getenv('APP_DEBUG'), getenv('APP_NAME'));
// ...

现在,您可以使用 PHP 的内置 Web 服务器,在新的应用程序环境变量前加上前缀:

$ APP_NAME=site php -S 127.0.0.1:8000 -t public
$ APP_NAME=admin php -S 127.0.0.1:8001 -t public
$ APP_NAME=api php -S 127.0.0.1:8002 -t public    

每个应用程序执行命令

├── bin/
│   └── console.php

添加一个新的控制台选项 --kernel 以便能够 运行 来自不同应用程序的命令:

bin/console

// ...
$name = $input->getParameterOption(['--kernel', '-k'], getenv('APP_NAME') ?: 'site');

//...
$kernel = new \VirtualKernel($env, $debug, $name);
$application = new Application($kernel);
$application
    ->getDefinition()
    ->addOption(new InputOption('--kernel', '-k', InputOption::VALUE_REQUIRED, 'The kernel name', $kernel->getName()))
;
$application->run($input);

稍后,使用此选项 运行 与默认命令 (site) 不同的任何命令。

$ bin/console about -k=api

或者,如果您愿意,可以使用环境变量:

$ export APP_NAME=api
$ bin/console about                         # api application
$ bin/console debug:router                  # api application
$
$ APP_NAME=admin bin/console debug:router   # admin application

您还可以在.env 文件中配置默认​​的APP_NAME 环境变量。

运行 每个应用程序测试

├── tests/
│   ├── Admin/
│   │   └── AdminWebTestCase.php
│   ├── Api/
│   ├── Site/

tests 目录与 src 目录非常相似,只需更新 composer.json 以将每个目录 tests/<Name>/ 映射到其 PSR-4 命名空间:

"autoload-dev": {
    "psr-4": {
        "Admin\Tests\": "tests/Admin/",
        "Api\Tests\": "tests/Api/",
        "Site\Tests\": "tests/Site/"
    }
},

同样,运行 composer dump-autoload 重新生成自动加载配置。

在这里,您可能需要为每个应用程序创建一个 <Name>WebTestCase class 以便一起执行所有测试:

test/Admin/AdminWebTestCase

namespace Admin\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

abstract class AdminWebTestCase extends WebTestCase
{
    protected static function createKernel(array $options = array())
    {
        return new \VirtualKernel(
            isset($options['environment']) ? $options['environment'] : 'test',
            isset($options['debug']) ? $options['debug'] : true,
            'admin'
        );
    }
}

稍后,从 AdminWebTestCase 扩展到测试 admin.company.com 应用程序(对其他应用程序执行相同的操作)。

生产和虚拟主机

为生产服务器和开发机器中的每个虚拟主机配置设置环境变量APP_NAME

<VirtualHost company.com:80>       
    SetEnv APP_NAME site

    # ...
</VirtualHost>

<VirtualHost admin.company.com:80>        
    SetEnv APP_NAME admin

    # ...
</VirtualHost>

<VirtualHost api.company.com:80>
    SetEnv APP_NAME api

    # ...
</VirtualHost>

正在向项目添加更多应用程序

通过三个简单的步骤,您应该能够向当前项目添加新的 vKernel/applications:

  1. configsrctests 目录添加一个新文件夹,其中包含应用程序的 <name> 及其内容。
  2. 至少将 bundles.php 文件添加到 config/<name>/ 目录。
  3. src/<Name>/tests/<Name> 目录添加新的 PSR-4 命名空间 composer.json autoload/autoload-dev 部分并更新自动加载配置文件。

检查新应用运行ning bin/console about -k=<name>.

最终目录结构:

├── bin/
│   └── console.php
├── config/
│   ├── admin/
│   │   ├── packages/
│   │   ├── bundles.php
│   │   ├── routes.yaml
│   │   ├── security.yaml
│   │   └── services.yaml
│   ├── api/
│   ├── site/
│   ├── packages/
│   ├── bundles.php
├── public/
│   └── index.php
├── src/
│   ├── Admin/
│   ├── Api/
│   ├── Site/
│   └── VirtualKernel.php
├── tests/
│   ├── Admin/
│   │   └── AdminWebTestCase.php
│   ├── Api/
│   ├── Site/
├── var/
│   ├── cache/
│   │   ├── admin/
│   │   │   └── dev/
│   │   │   └── prod/
│   │   ├── api/
│   │   └── site/
│   └── log/
├── .env
├── composer.json

不同于多个内核文件的方法,这个版本减少了很多代码重复和文件;由于环境变量和虚拟内核 class.

,所有应用程序只需一个内核 index.phpconsole

基于 Symfony 4 框架的示例:https://github.com/yceruto/symfony-skeleton-vkernel 灵感来自 https://symfony.com/doc/current/configuration/multiple_kernels.html

另外,当你想要 运行 Behat 测试时,你应该 运行 使用这个命令:

对于 windows:

set APP_NAME=web&& vendor\bin\behat

对于linux:

export APP_NAME='web' && vendor\bin\behat

其中 "web" 是您想要 运行 的内核名称。

KernelInterface::getName() 方法和 kernel.name 参数已被弃用。他们别无选择,因为这是一个在 Symfony 应用程序中不再有意义的概念。

如果您需要应用程序内核的独特 ID,可以使用 KernelInterface::getContainerClass() 方法和 kernel.container_class 参数。

同样,getRootDir() 方法和 kernel.root_dir 参数也已弃用。另一种方法是使用 Symfony 3.3

中引入的 getProjectdir() 和 kernel.project_dir 方法

https://symfony.com/blog/new-in-symfony-4-2-important-deprecations#deprecated-the-kernel-name-and-the-root-dir