Completed
Pull Request — master (#10)
by Alex
06:03
created

StepsPass::processStepAction()   B

Complexity

Conditions 8
Paths 35

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 7.8464
c 0
b 0
f 0
cc 8
nc 35
nop 4
1
<?php
2
3
/**
4
 * This file is part of the PierstovalCharacterManagerBundle package.
5
 *
6
 * (c) Alexandre Rock Ancelet <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Pierstoval\Bundle\CharacterManagerBundle\DependencyInjection\Compiler;
13
14
use Doctrine\Common\Inflector\Inflector;
15
use Doctrine\Common\Persistence\ObjectManager;
16
use Pierstoval\Bundle\CharacterManagerBundle\Action\AbstractStepAction;
17
use Pierstoval\Bundle\CharacterManagerBundle\Action\StepActionInterface;
18
use Pierstoval\Bundle\CharacterManagerBundle\Registry\ActionsRegistry;
19
use Pierstoval\Bundle\CharacterManagerBundle\Resolver\StepActionConfigurator;
20
use Pierstoval\Bundle\CharacterManagerBundle\Resolver\StepResolverInterface;
21
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
22
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
23
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
24
use Symfony\Component\DependencyInjection\ContainerBuilder;
25
use Symfony\Component\DependencyInjection\Definition;
26
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
27
use Symfony\Component\DependencyInjection\Reference;
28
use Symfony\Component\Routing\RouterInterface;
29
use Symfony\Contracts\Translation\TranslatorInterface;
30
use Twig\Environment;
31
32
/**
33
 * Check that every action class extends the right action interface.
34
 * And for each step-tagged service, if the class extends the provided abstract class,
35
 *  calls some useful methods.
36
 */
37
class StepsPass implements CompilerPassInterface
38
{
39
    public const PARAMETERS_MANAGERS = 'pierstoval_character_manager.managers';
40
41
    /**
42
     * {@inheritdoc}
43
     */
44
    public function process(ContainerBuilder $container)
45
    {
46
        $this->validateManagers($container);
47
        $this->processConfiguredServices($container);
48
    }
49
50
    private function validateManagers(ContainerBuilder $container): void
51
    {
52
        $managers = $container->getParameter(static::PARAMETERS_MANAGERS);
53
54
        foreach ($managers as $name => $config) {
55
            $config['name'] = $name;
56
            $managers[$name] = $this->validateManagerSteps($config, $container);
57
        }
58
59
        $container->setParameter(static::PARAMETERS_MANAGERS, $managers);
60
    }
61
62
    /**
63
     * Validate steps defined in configuration, and makes sure all "action" classes are instances of StepActionInterface
64
     */
65
    private function validateManagerSteps(array $managerConfiguration, ContainerBuilder $container): array
66
    {
67
        /** @var array[] $steps */
68
        $steps = $managerConfiguration['steps'];
69
70
        $stepNumber = 1;
71
72
        // Validate steps that can't be validated in Configuration.
73
        // First loop mandatory because we re-validate each step dependency in another loop after normalization.
74
        foreach ($steps as $id => $step) {
75
            $name = $step['name'] = $id;
76
77
            if (!$step['label']) {
78
                $step['label'] = $this->generateStepLabel($step['name']);
79
            }
80
81
            $step['manager_name'] = $managerConfiguration['name'];
82
83
            $step['number'] = $stepNumber++;
84
85
            $steps[$name] = $step;
86
        }
87
88
        foreach ($steps as $name => $step) {
89
            // Validate steps to disable on change, to be sure each step is defined.
90
            foreach ($step['onchange_clear'] as $stepToDisable) {
91
                if (!array_key_exists($stepToDisable, $steps)) {
92
                    throw new InvalidConfigurationException(sprintf(
93
                        'Step to disable must be a valid step name, "%s" given.'."\n".
94
                        'Available steps: %s',
95
                        $stepToDisable, implode(', ', array_keys($steps))
96
                    ));
97
                }
98
            }
99
100
            // Validate steps dependencies, to be sure each step is defined.
101
            foreach ($step['dependencies'] as $stepDependency) {
102
                if (!array_key_exists($stepDependency, $steps)) {
103
                    throw new InvalidConfigurationException(sprintf(
104
                        'Step dependency must be a valid step name, "%s" given.'."\n".
105
                        'Available steps: %s',
106
                        $stepDependency, implode(', ', array_keys($steps))
107
                    ));
108
                }
109
            }
110
111
            // Validate step actions.
112
            $action = $step['action'];
113
114
            // Check if action defined as a service or as a simple class.
115
            $class = $container->has($action) ? $container->getDefinition($action)->getClass() : $action;
116
117
            if (!class_exists($class) || !is_a($class, StepActionInterface::class, true)) {
118
                throw new InvalidArgumentException(sprintf(
119
                    'Step action must be a valid class implementing %s. "%s" given.',
120
                    StepActionInterface::class, class_exists($class) ? $class : \gettype($class)
121
                ));
122
            }
123
        }
124
125
        // And update all steps.
126
        $managerConfiguration['steps'] = $steps;
127
128
        return $managerConfiguration;
129
    }
130
131
    /**
132
     * Automatically convert the actions into services.
133
     * If they're defined as classes, this has the advantage to autowire them, etc.
134
     *
135
     * @param ContainerBuilder $container
136
     */
137
    private function processConfiguredServices(ContainerBuilder $container)
138
    {
139
        if (!$container->hasDefinition(ActionsRegistry::class)) {
140
            throw new InvalidConfigurationException('Step actions registry not set in your configuration. Maybe the extension was not processed properly?');
141
        }
142
143
        $registryDefinition = $container->getDefinition(ActionsRegistry::class);
144
145
        foreach ($container->getParameter(static::PARAMETERS_MANAGERS) as $managerName => $config) {
146
            $this->processStepAction($managerName, $config, $registryDefinition, $container);
147
        }
148
    }
149
150
    private function processStepAction(string $managerName, array $config, Definition $registryDefinition, ContainerBuilder $container): void
151
    {
152
        /** @var array[] $finalSteps */
153
        $finalSteps = $config['steps'];
154
155
        foreach ($finalSteps as $step) {
156
            $action = $step['action'];
157
            if ($container->has($action)) {
158
                /** @var  $definition */
159
                $definition = $container->getDefinition($action);
160
            } else {
161
                // If action is not yet a service, it means it's a class name.
162
                // In this case, we create a new service.
163
                $definition = new Definition($action);
164
                $definition
165
                    ->setPrivate(true)
166
                    ->setAutowired(true)
167
                ;
168
                $container->setDefinition($action, $definition);
169
            }
170
171
            $definition
172
                ->addMethodCall('configure', [
173
                    $managerName,
174
                    $step['name'],
175
                    $config['character_class'],
176
                    new Reference(StepResolverInterface::class),
177
                ])
178
            ;
179
180
            // If class extends the abstract one, we inject some cool services.
181
            if (is_a($definition->getClass(), AbstractStepAction::class, true)) {
182
                if ($container->hasDefinition(ObjectManager::class)) {
183
                    $definition->addMethodCall('setObjectManager', [new Reference(ObjectManager::class)]);
184
                }
185
                if ($container->hasDefinition(Environment::class)) {
186
                    $definition->addMethodCall('setTwig', [new Reference(Environment::class)]);
187
                }
188
                if ($container->hasDefinition(RouterInterface::class)) {
189
                    $definition->addMethodCall('setRouter', [new Reference(RouterInterface::class)]);
190
                }
191
                if ($container->hasDefinition(TranslatorInterface::class)) {
192
                    $definition->addMethodCall('setTranslator', [new Reference(TranslatorInterface::class)]);
193
                }
194
            }
195
196
            // Finally add the step action to the registry
197
            $registryDefinition->addMethodCall('addStepAction', [$managerName, $step['name'], new ServiceClosureArgument(new Reference($action))]);
198
        }
199
    }
200
201
    private function generateStepLabel(string $name): string
202
    {
203
        return Inflector::ucwords(trim(str_replace(['.', '_', '-'], ' ', $name)));
204
    }
205
}
206