Более одного приложения на репозиторий проекта с Symfony 4

У меня есть три старых приложения (работающих на Symfony 2), каждое из которых было разработано в отдельных репозиториях git и настроено на соответствующих виртуальных хостах:

  1. company.com Сайт компании.
  2. admin.company.com Администрация сайта.
  3. api.company.com Сервис компании API.

Несмотря на то, что они используют одну и ту же базу данных. Поэтому мы решили (Компания) объединить их все в одном приложении со структурой и подходом 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).

Возможно ли иметь много приложений в одном репозитории проектов, работающих независимо (как отдельные приложения), но с общей конфигурацией? Как лучше всего это сделать?

Заранее спасибо.


person Community    schedule 28.08.2017    source источник


Ответы (4)


Вероятно, подход несколько ядер может быть хорошим вариантом для решения такого рода проектов, но, думая сейчас о подходе Symfony 4 с переменными среды, структурой и реализацией ядра, его можно было бы улучшить.

Виртуальное ядро ​​на основе имени

Термин «Виртуальное ядро» относится к практике запуска более одного приложения (например, api.example.com и admin.example.com) в одном репозитории проекта. Виртуальные ядра основаны на именах, что означает, что у вас есть несколько имен ядер, работающих в каждом приложении. Тот факт, что они работают в одном и том же физическом репозитории проекта, не очевиден для конечного пользователя.

Короче говоря, каждое имя ядра соответствует одному приложению.

Конфигурация на основе приложения

Во-первых, вам потребуется скопировать структуру одного приложения для каталогов config, src, var и оставить корневую структуру для общих пакетов и конфигурации. Это должно выглядеть так:

├── 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, в то время как переменные среды решают, какую среду разработки и режим отладки следует использовать для запуска вашего приложения, вы можете добавить новую переменную среды APP_NAME, чтобы настроить выполнение приложения:

public/index.php

// ...

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

А пока вы можете поиграть с ним, используя встроенный в PHP веб-сервер, добавив префикс к новой переменной среды приложения:

$ 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

Также вы можете настроить переменную среды по умолчанию 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 для каждой конфигурации виртуального хоста на рабочем сервере и компьютере для разработки:

<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. Добавьте в разделы composer.json autoload/autoload-dev новые пространства имен PSR-4 для каталогов 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

person yceruto    schedule 28.08.2017
comment
Спасибо, что нашли время написать такой подробный ответ, очень помогли мне! - person Sean; 28.01.2021

Вы можете создавать новые среды, такие как: admin, website, api. Затем, предоставив переменную среды SYMFONY_ENV с помощью apache/nginx, вы сможете запускать специальное приложение и по-прежнему использовать поддомены company.com, admin.company.com, api.company.com. Также вы сможете легко загрузить только необходимую маршрутизацию.

В зависимости от того, сколько приложений вы хотите создать на основе этого подхода, вы можете добавить условия для загрузки указанных пакетов по проекту в классе AppKernel или создать отдельные классы для каждого проекта.

Вам также следует прочитать эту статью https://jolicode.com/blog/multiple-applications-with-symfony2

person Zbigniew Pretki    schedule 28.08.2017

Также, если вы хотите запустить тестирование Behat, вы должны запустить его с помощью этой команды:

для окон:

set APP_NAME=web&& vendor\bin\behat

для линукса:

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

где «web» — это имя вашего ядра, которое вы хотите запустить.

person Frantisek    schedule 18.09.2018

Метод KernelInterface::getName() и параметр kernel.name устарели. Альтернативы им нет, потому что эта концепция больше не имеет смысла в приложениях Symfony.

Если вам нужен отличительный идентификатор ядра приложения, вы можете использовать метод KernelInterface::getContainerClass() и параметр kernel.container_class.

Точно так же устарели метод getRootDir() и параметр kernel.root_dir. Альтернативой является использование методов getProjectdir() и kernel.project_dir, представленных в Symfony 3.3.

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

person yakupars    schedule 12.03.2019