7

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

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

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

现在,我正在按计划将所有内容集成到一个应用程序/存储库中,但我开始处理一些性能和结构问题:

  • 因为我只有一个入口点,index.php所以我做了两个路由前缀以便能够访问子应用程序company.com/admin/company.com/api/所以每次都会加载所有路由:(
  • 对于每个请求,所有包和配置都是不必要的加载和处理。例如:当我访问 API 路径时,SonataAdminBundle它也会被加载:(
  • 缓存清除命令需要很长时间才能完成。
  • 测试正在崩溃,现在也需要很长时间才能完成。

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

  1. company.com仅为公司网站加载捆绑包、路由和配置 ( SwiftmailerBundle, ...)
  2. admin.company.com加载包、路由和配置仅用于网站管理 ( SecurityBundle, SonataAdminBundle, ...)
  3. api.company.com仅加载捆绑包、路由和配置以提供快速的 API 公司服务 ( SecurityBundle, FOSRestBundle, NelmioApiDocBundle, ...)

到目前为止,这就是我正在做的事情:

// 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 定义变量)我想清除每个的缓存“子应用程序”等等。

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

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

提前致谢。

4

4 回答 4

23

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

基于名称的虚拟内核

术语“虚拟内核”是指在单个项目存储库上运行多个应用程序(例如api.example.com和)的做法。admin.example.com虚拟内核是“基于名称的”,这意味着您在每个应用程序上运行多个内核名称。它们在同一个物理项目存储库上运行的事实对最终用户来说并不明显。

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

基于应用的配置

首先,您需要为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这是支持先前结构的类的完整示例:

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类需要一个额外的参数 ( name) 来定义要加载的应用程序。为了让自动加载器找到你的新\VirtualKernel类,确保将它添加到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

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

每个应用程序运行测试

├── 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类,以便一起执行所有测试:

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为生产服务器和开发机器中的每个 vhost 配置设置环境变量:

<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/应用程序添加到当前项目:

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

检查正在运行的新应用程序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

不同于多内核文件的方式,这个版本减少了大量的代码重复和文件;由于环境变量和虚拟内核类,只有一个内核,index.php并且适用于所有应用程序。console

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

于 2017-08-28T22:14:54.153 回答
1

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

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

您还应该阅读这篇文章https://jolicode.com/blog/multiple-applications-with-symfony2

于 2017-08-28T19:31:12.540 回答
0

此外,当您想要运行 Behat 测试时,您应该使用以下命令运行它:

对于窗户:

set APP_NAME=web&& vendor\bin\behat

对于 Linux:

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

其中“web”是您要运行的内核名称。

于 2018-09-18T11:40:12.020 回答
0

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

于 2019-03-12T12:43:12.213 回答