Symfony DI и как его готовить
IoC(Inversion of Control)
Инверсия управления (IoC) является принципом разработки, в котором пользовательские части компьютерной программы получают поток управления из общей системы.

/**
* Class TestComand
*/
class Echoer
{
private $device;
private $formatter;
private $corrector;
private $transformator;
/**
* Set device
*
* @param mixed $device device
*
* @return void
*/
public function setDevice($device)
{
$this->device = $device;
}
/**
* Set formatter
*
* @param mixed $formatter formatter
*
* @return void
*/
public function setFormatter($formatter)
{
$this->formatter = $formatter;
}
/**
* Set corrector
*
* @param mixed $corrector corrector
*
* @return void
*/
public function setCorrector($corrector)
{
$this->corrector = $corrector;
}
/**
* Set transformator
*
* @param mixed $transformator transformator
*
* @return void
*/
public function setTransformator($transformator)
{
$this->transformator = $transformator;
}
public function echo($voice)
{
$this->corrector->correct($voice);
$this->transformator->transform($voice);
$this->formatter->format($voice);
$this->device->play($voice);
}
}$echoer = new Echoer();
$echoer->setCorrector(new MyCorrector());
$echoer->setFormatter(new MyFormatter());
$echoer->setTransformator(new MyTransformattor());
$echoer->echo("Hello world!");Никакого IoC!
$echoer = new Echoer();
foreach ($arguments as $name => $argument) {
$setterName = 'set' . ucwords($name);
$echoer->$setterName($locator->get($argument));
}
$echoer->echo("Hello world!");А вот это уже IoC !
the Hollywood Principle - "Don't call us, we'll call you"

DI (Dependency Injection)
Процесс предоставления внешней зависимости программному компоненту


Symfony container
The DependencyInjection Component
Позволяет централизованно и стандартизованно определять как будут созданы объекты в вашем приложении
LazyInitialization
+
Factory method
=
Service Container
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="alfaforex.partnership.deal_collector"
alias="alfaforex.partnership.deal_collector.tps"
public="true" />
<service id="alfaforex.partnership.deal_collector.test"
class="Alfaforex\Partnership\Test\PredefinedDealCollector"
decorates="alfaforex.partnership.deal_collector.tps"
decoration-inner-name="alfaforex.partnership.test.tps.inner"
decoration-priority="100000"
public="true">
<argument type="service" id="alfaforex.partnership.test.tps.inner" />
</service>
<service id="alfaforex.partnership.reward_calculator.commission"
alias="alfaforex.partnership.reward_calculator.commission.test"
public="true" />
<service id="alfaforex.partnership.reward_calculator.commission.test"
class="Alfaforex\Partnership\Test\FutileRewardCalculator">
</service>
<service id="alfaforex.partnership.reward_calculator.commission.excluding.customer_status.test"
public="true"
class="Alfaforex\Partnership\Test\FutileDelegatingRewardCalculator"
decorates="alfaforex.partnership.reward_calculator.commission.excluding.customer_status"
decoration-priority="10000">
<argument type="service" id="alfaforex.partnership.reward_calculator.commission.excluding.customer_status.inner"/>
</service>
</services>
</container>Настройка сервисов
Любой объект можно (нужно!) определять как сервис
<service id="alfaforex.partnership.command.calculate"
class="Alfaforex\Partnership\Command\CalculateCommand"
public="false">
<argument>%kernel.name%</argument>
<tag name="console.command" />
<tag name="alfaforex.key_value.aware" module="partnership" />
</service>/**
* Class CalculateCommand
*
* @Cron(minute="@/5", parameters={"--period": "minute"})
*/
class CalculateCommand extends Command
{
/**
* This application name
*
* @var string
*/
private $appName;
/**
* CalculateCommand constructor.
*
* @param string $appName Application name
*/
public function __construct(string $appName)
{
parent::__construct();
$this->appName = $appName;
}
/**
* @inheritDoc
*/
protected function configure()
{
parent::configure();
$this->setName('partnership:calculate')
->addOption('period', null, InputOption::VALUE_REQUIRED, 'Period of aggregation: minute, week, month', '')
->addArgument('from', InputArgument::OPTIONAL, 'Period start time, in ISO-8601 form', '')
->addArgument('to', InputArgument::OPTIONAL, 'Period end time, in ISO-8601 form', '')
->addCommands(
[
new SubCommand('partnership:calculate-reward'),
new SubCommand('import:import-to-aflk'),
new SubCommand('payment:aggregate-partner-report'),
]
);
}
/**
* @inheritDoc
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$lockHandler = new LockHandler(
sprintf('%s-%s-%s', $this->appName, $this->getName(), $input->getOption('period') ?: '')
);
if (!$lockHandler->lock(false)) {
$output->writeln('Task is already running');
return;
}
try {
$dates = $this->resolveDate(
$input->getOption('period'),
$input->getArgument('from'),
$input->getArgument('to')
);
parent::execute($this->createInput($input, $dates[0], $dates[1]), $output);
$storeDates = $input->getOption('period') === self::PERIOD_MINUTE;
if ($storeDates) {
$this->settings['reward_calculate_minute_from'] = $dates[1];
$this->settings->saveSettings();
}
} finally {
$lockHandler->release();
}
}
}
Команды
<service id="alfaforex.payment.controller.reward"
class="Alfaforex\PaymentBundle\Controller\RewardController"
public="true">
<argument type="service" id="alfaforex.payment.service.reward" />
</service>/**
* Class RewardController
*/
class RewardController
{
/**
* Reward service
*
* @var RewardService
*/
private $rewardsService;
/**
* RewardController constructor.
*
* @param RewardService $rewardsService Service that handles all of the rewards stuff
*/
public function __construct(RewardService $rewardsService)
{
$this->rewardsService = $rewardsService;
}
}
Контроллеры
<service id="alfaforex.partnership.services.money_formatter"
class="Money\Formatter\DecimalMoneyFormatter"
public="false">
<argument type="service" id="alfaforex.partnership.services.money_formatter.currencies"/>
</service>
<service id="alfaforex.partnership.services.money_formatter.currencies"
class="Money\Currencies\ISOCurrencies"
public="false"/>
<service id="alfaforex.partnership.services.money_parser"
class="Money\Parser\DecimalMoneyParser"
public="false">
<argument type="service">
<service class="Money\Currencies\ISOCurrencies"/>
</argument>
</service>
<service id="alfaforex.partnership.reward_calculator.commission.abstract"
abstract="true"
public="false">
<argument type="service" id="logger"/>
<argument type="service" id="alfaforex.partnership.services.money_formatter" />
</service>И даже вендорские классы

И правда, зачем?!
Так надо!
- Легче создавать экземпляры
- Рациональное переиспользование классов
-Тесты
Конфигурирование бандла

Поиск закешированного контейнера
Сборка конфигурации приложения
Конфигурирование каждого бандла (Extension)
запуск CompillerPass
Финализация и кеширование получившегося контейнера
Сборка контейнера
Поиск закешированного контейнера
Сборка конфигурации приложения
Конфигурирование каждого бандла (Extension)
запуск CompillerPass
Финализация и кеширование получившегося контейнера
Сборка контейнера
Поиск закешированного контейнера
Сборка конфигурации приложения
Конфигурирование каждого бандла (Extension)
запуск CompillerPass
Финализация и кеширование получившегося контейнера
Сборка контейнера
Configuration
Extension
CompillerPass
Configuration

Нормализация
Валидация
Дефолтные значения
$tree = new TreeBuilder();
$root = $tree->root('alfaforex_payment');
$root
->addDefaultsIfNotSet()
->children()
->scalarNode('entity_manager')
->isRequired()
->canNotBeEmpty()
->info('Main service DB entity manager')
->end()
->integerNode('min_payment_approvals')
->isRequired()
->min(1)
->info('Minimum number of approvals for payment per each customer')
->end()
->end();
return $tree;Проще не бывает
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('scheduler');
$rootNode
->children()
->booleanNode('enabled')->defaultValue($this->enabled)->end()
->arrayNode('schedule')
->validate()
->ifTrue(function($v) {
return !isset($v['entity_managers']) || empty($v['entity_managers']); })
->thenInvalid('"entity_managers" option is not set')
->end()
->validate()
->ifTrue(function($v) { return !isset($v['default_entity_manager']); })
->thenInvalid('"default_entity_manager" is not set')
->end()
->validate()
->ifTrue(function($v) {
$entityManagers = $v['entity_managers'];
$defaultEntityManager = $v['default_entity_manager'];
return !isset($entityManagers[$defaultEntityManager]);
})
->thenInvalid('"default_entity_manager" has to be one of "entity_managers"')
->end()
->validate()
->ifTrue(function($v) {
return !isset($v['queues']) || empty($v['queues']); })
->thenInvalid('"queues" option is not set')
->end()
->validate()
->ifTrue(function($v) { return !isset($v['default_queue']); })
->thenInvalid('"default_queue" is not set')
->end()
->validate()
->ifTrue(function($v) {
$entityManagers = $v['entity_managers'];
$defaultEntityManager = $v['default_entity_manager'];
return !isset($entityManagers[$defaultEntityManager]);
})
->thenInvalid('"default_entity_manager" has to be one of "entity_managers"')
->end()
->fixXmlConfig('entity_manager')
->fixXmlConfig('forced_command')
->fixXmlConfig('queue')
->children()
->append($this->getForcedCommandsNode())
->scalarNode('default_entity_manager')->end()
->arrayNode('entity_managers')
->useAttributeAsKey('name')
->normalizeKeys(false)
->prototype('scalar')->end()
->end()
->scalarNode('default_queue')->end()
->arrayNode('queues')
->useAttributeAsKey('name')
->normalizeKeys(false)
->prototype('scalar')->end()
->end()
->end()
->end()
->end();
return $treeBuilder;Бывает и так
->beforeNormalization()
->always()
->then(function ($v) {
if (isset($v['schedule']) && !isset($v['schedule_enabled']) ) {
$v['schedule_enabled'] = true;
} elseif (!isset($v['schedule']) && !isset($v['schedule_enabled'])) {
$v['schedule_enabled'] = false;
}
return $v;
})
->end()Перед нормализацией
->arrayNode('schedule')
->validate()
->ifTrue(function($v) {
return !isset($v['entity_managers']) || empty($v['entity_managers']); })
->thenInvalid('"entity_managers" option is not set')
->end()
->validate()
->ifTrue(function($v) { return !isset($v['default_entity_manager']); })
->thenInvalid('"default_entity_manager" is not set')
->end()
->validate()
->ifTrue(function($v) {
$entityManagers = $v['entity_managers'];
$defaultEntityManager = $v['default_entity_manager'];
return !isset($entityManagers[$defaultEntityManager]);
})
->thenInvalid('"default_entity_manager" has to be one of "entity_managers"')
->end()
->validate()
->ifTrue(function($v) {
return !isset($v['queues']) || empty($v['queues']); })
->thenInvalid('"queues" option is not set')
->end()
->validate()
->ifTrue(function($v) { return !isset($v['default_queue']); })
->thenInvalid('"default_queue" is not set')
->end()
->validate()
->ifTrue(function($v) {
$entityManagers = $v['entity_managers'];
$defaultEntityManager = $v['default_entity_manager'];
return !isset($entityManagers[$defaultEntityManager]);
})
->thenInvalid('"default_entity_manager" has to be one of "entity_managers"')
->end()Валидация
->fixXmlConfig('entity_manager')
->arrayNode('entity_managers')
->useAttributeAsKey('name')
->normalizeKeys(false)
->prototype('scalar')->end()
->end()При нормализации все "-" заменяются "_"
->fixXmlConfig('entity_manager')
->arrayNode('entity_managers')
->useAttributeAsKey('name')
->normalizeKeys(false)
->prototype('scalar')->end()
->end()$treeBuilder->root('acme_config')
->children()
->arrayNode('array_prototype')
->fixXmlConfig('parameter')
->children()
->arrayNode('parameters')
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('value')->isRequired()->end()
->end()
->end()
->end()
->end()
->end()
->end();<config ...>
<array-prototype>
<parameter name="foo">
<value>fiz</value>
</parameter>
</array-prototype>
</config>...
->children()
->append($this->getForcedCommandsNode())
... private function getForcedCommandsNode()
{
$builder = new TreeBuilder();
$node = $builder->root('forced_commands');
$node
->prototype('scalar')->end()
->defaultValue([])
->treatNullLike([])
->end();
return $node;
}Добавление секций
Extension

Получение конфигурации
Композиция параметров бандла
Композиция сервисов

Разворошим его полностью!
public function load(array $configs, ContainerBuilder $container)
{
$config = $this->processConfiguration(new Configuration(), $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('config.xml');
$container->getDefinition('alfaforex.partnership.deal_collector.tps')
->replaceArgument(1, $config['instances']);
$container->getDefinition('alfaforex.partnership.reward_calculator.commission.excluding.bond')
->replaceArgument(2, $config['instances']);
if ($container->getParameter('kernel.environment') === 'test') {
$loader->load('config_test.xml');
}
$container->getDefinition('alfaforex.partnership.deal_collector.splitted')
->replaceArgument(1, $config['max_data_window_size']);
$container->getDefinition('alfaforex.partnership.reward_calculator.commission.excluding.multi_condition')
->replaceArgument(1, array_flip($config['exclude_account_types']));
$epochDate = \DateTime::createFromFormat('Y-m-d', $config['reward_commission_epoch']);
$epochDate->setTime(0, 0, 0, 0);
$aflkConnection = new Reference('doctrine.dbal.af_lk_connection');
$mainEm = new Reference(sprintf('doctrine.orm.%s_entity_manager', $config['entity_manager']));
$container->getDefinition('alfaforex.partnership.services.pamm_service')
->replaceArgument(0, $aflkConnection)
->replaceArgument(1, $config['termless_partners'])
->replaceArgument(2, $epochDate->format(\DateTime::RFC3339));
}Соберите зависимости сервисов из конфига
<?xml version="1.0" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="alfaforex.partnership.services.pamm_service"
class="Alfaforex\Partnership\Services\PammService"
public="false">
<argument/> <!-- LK Connection -->
<argument type="collection"/> <!-- List of termless partners -->
<argument type="string"/> <!-- Commission payment epoch start date Y-m-d -->
</service>
</services>
</container>
Предела нет

Предела нет ?
Настройки чужих бандлов не доступны

PrependExtensionInterface
foreach ($container->getExtensions() as $extension) {
if ($extension instanceof PrependExtensionInterface) {
$extension->prepend($container);
}
}/**
* Class AlfaforexCoreExtension
*/
class AlfaforexCoreExtension extends Extension implements PrependExtensionInterface
{
/**
* Flag if we have doctrine extension
*
* @var bool
*/
private $hasDoctrine;
/**
* @inheritDoc
*/
public function prepend(ContainerBuilder $container)
{
$this->hasDoctrine = $container->hasExtension('doctrine');
if ($container->hasExtension('jms_serializer')) {
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/serializer'));
$loader->load('jms_config.yml');
}
}
}
/**
* {{@inheritdoc}}
*/
public function prepend(ContainerBuilder $container)
{
$cryptors = $this->findCryptors($container);
foreach ($cryptors as $cryptor) {
$decorator = new DefinitionDecorator('health_check.encryptor_check');
$decorator->setClass(EncryptorHealthCheck::class);
$decorator->addTag('crypt.encryptor.aware', [ 'cryptor_name' => $cryptor ]);
$decorator->addTag('crypt.decryptor.aware', [ 'cryptor_name' => $cryptor ]);
$decorator->addTag('liip_monitor.check');
$container->setDefinition('health_check.encryptor_check.' . $cryptor, $decorator);
}
}
CompillerPass
CompilerPass даeт вам возможность манипулировать другими сервисами, которые были зарегистрированы в контейнере. Вызывается после загрузки всех расширений, он позволяет редактировать определения служб других расширений, а также извлекать информацию об определениях служб.

/**
* @inheritDoc
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new TokenAwareCompilerPass());
$container->addCompilerPass(new DoctrineDbalCompilerPass());
$container->addCompilerPass(new RemoveOverwrittenServicesCompilerPass());
$container->addCompilerPass(new ExceptionDataProviderCompilerPass());
$container->addCompilerPass(new ExceptionDescriberCompilerPass());
$container->addCompilerPass(new ConfigureExceptionsCompilerPass());
$container->addCompilerPass(new VerifyDoctrineRepositoriesCompilerPass());
$container->addCompilerPass(new ReplaceApcWithApcuCachesPass());
$container->addCompilerPass(new RequestParametersExtractorCompilerPass());
$container->addCompilerPass(new DecorateDoctrineEmCompilerPass());
$container->addCompilerPass(new ResetterCompilerPass());
$container->addCompilerPass(new AddPsrCacheCheckPass());
$container->addCompilerPass(new AddArraySupportForParamConverterCompilerPass());
$container->addCompilerPass(new ReplaceJmsDateHandler());
$container->addCompilerPass(new FixEpwtArrayCompilerPass());
$container->addCompilerPass(new DefineTimeMeasurementForSqlLoggerCompilerPass());
$container->addCompilerPass(new DefineTimeMeasurementCacheCompilerPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new DefineTimeMeasurementForGuzzleCompilerPass());
$container->addCompilerPass(new DefineTimeMeasurementForTemplatesCompilerPass());
}Обработать специальным образом затегированные сервисы
/**
* @inheritDoc
*/
public function process(ContainerBuilder $container)
{
$checks = [];
foreach ($container->findTaggedServiceIds('cache.pool') as $serviceId => $attributes) {
$definition = $container->getDefinition($serviceId);
if ($definition->isAbstract()) {
continue;
}
$checkDefinition = new Definition(PsrCacheCheck::class, [$serviceId, new Reference($serviceId)]);
$checkDefinition->setPublic(false);
$id = sprintf('alfaforex.core.monitoring.cache.%s', md5($serviceId));
$container->setDefinition($id, $checkDefinition);
$checks[] = new Reference($id);
}
$container->getDefinition('alfaforex.core.monitoring.psr6_collection')->replaceArgument(0, $checks);
}<service id="cache.app" parent="cache.adapter.filesystem" public="true">
<tag name="cache.pool" clearer="cache.app_clearer" />
</service>
<service id="cache.system" parent="cache.adapter.system" public="true">
<tag name="cache.pool" />
</service>
<service id="cache.validator" parent="cache.system" public="false">
<tag name="cache.pool" />
</service>
<service id="cache.serializer" parent="cache.system" public="false">
<tag name="cache.pool" />
</service>
<service id="cache.annotations" parent="cache.system" public="false">
<tag name="cache.pool" />
</service>
public function process(ContainerBuilder $container)
{
$definitions = $container->getDefinitions();
foreach ($definitions as $id => $definition) {
$ref = new \ReflectionClass($definition->getClass());
if (!$ref->implementsInterface("CompanyAware")) {
continue;
}
$definition->setCompany( new Reference("company"));
}
}Добавить зафисимость по интерфейсу типа Aware
/**
* Class contain service function for accounts
*/
final class AccountService implements CompanyAware
{
/**
* Company
*
* @var string
*/
private $company;
/**
* {@inheritdoc}
*/
public function setCompany($company)
{
$this->company = $company;
}
}Предела нет !


Symfony DI и как его готовить
By faecie
Symfony DI и как его готовить
- 320