【问题标题】:More than one application per project repository with Symfony 4使用 Symfony 4 的每个项目存储库有多个应用程序
【发布时间】:2017-08-28 19:00:10
【问题描述】:

我有三个旧应用程序(在 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 加载包、路由和配置仅用于网站管理(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)我想清除每个“子应用”的缓存等等。

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

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

提前致谢。

【问题讨论】:

    标签: php symfony symfony4 symfony-flex


    【解决方案1】:

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

    基于名称的虚拟内核

    术语“虚拟内核”是指在单个项目存储库上运行多个应用程序(例如api.example.comadmin.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/控制台

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

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

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

    检查运行bin/console about -k=&lt;name&gt;的新应用程序。

    最终目录结构:

    ├── 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.phpconsole

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

    【讨论】:

    • 感谢您花时间写出如此详细的答案,真的帮了我大忙!
    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

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

      对于窗户:

      set APP_NAME=web&& vendor\bin\behat
      

      对于 Linux:

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

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

      【讨论】:

        【解决方案4】:

        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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-09-07
          相关资源
          最近更新 更多