Вероятно, подход несколько ядер может быть хорошим вариантом для решения такого рода проектов, но, думая сейчас о подходе 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
a> вы можете выделить приложение для запуска с помощью выделенных файлов проекта (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/приложения в текущий проект:
- Добавьте в каталоги
config
, src
и tests
новую папку с <name>
приложением и его содержимым.
- Добавьте в каталог
config/<name>/
как минимум файл bundles.php
.
- Добавьте в разделы
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