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