Completed
Push — master ( 15ae22...677e94 )
by Gerrit
08:19
created

DecoratorTemplateCompilerPass::process()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 0
cts 13
cp 0
rs 9.472
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 12
1
<?php
2
/**
3
 * Copyright (C) 2018 Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 *
8
 * @license GPL-3.0
9
 *
10
 * @author Gerrit Addiks <[email protected]>
11
 */
12
13
namespace Addiks\SymfonyGenerics\DependencyInjection\Compiler;
14
15
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16
use Symfony\Component\DependencyInjection\ContainerBuilder;
17
use Webmozart\Assert\Assert;
18
use Symfony\Component\DependencyInjection\Definition;
19
use Symfony\Component\DependencyInjection\Reference;
20
use ErrorException;
21
22
/**
23
 * This compiler-pass enables to have one service that can decorate multiple different other services.
24
 * The most prominent use-case would be to attach the same error-handling to multiple different controllers.
25
 *
26
 * TODO: extract definition-filter and argument-filter into their own objects/classes.
27
 */
28
final class DecoratorTemplateCompilerPass implements CompilerPassInterface
29
{
30
31
    /**
32
     * @var string
33
     */
34
    private $tagNameDecorates;
35
36
    public function __construct(
37
        string $tagNameDecorates = "symfony_generics.decorates"
38
    ) {
39
        $this->tagNameDecorates = $tagNameDecorates;
40
    }
41
42
    public function process(ContainerBuilder $container): void
43
    {
44
        /** @var array<string, array<int, array<string, string>>> $decoratesTags */
45
        $decoratesTags = $container->findTaggedServiceIds($this->tagNameDecorates);
46
47
        foreach ($decoratesTags as $decoratorServiceId => $decorates) {
48
            /** @var array<array<string, string>> $decorates */
49
50
            foreach ($decorates as $decorate) {
51
                /** @var array<string, string> $decorate */
52
53
                Assert::keyExists($decorate, 'decorates', sprintf(
54
                    "Missing key 'decorates' (containing the decorated-service-id) on tag '%s' in decorator '%s'!",
55
                    $this->tagNameDecorates,
56
                    $decoratorServiceId
57
                ));
58
59
                /** @var string $decoratedServiceId */
60
                $decoratedServiceId = $decorate['decorates'];
61
62
                unset($decorate['decorates']);
63
64
                $this->applyDecorate($container, $decoratorServiceId, $decoratedServiceId, $decorate);
65
            }
66
67
            $container->removeDefinition($decoratorServiceId);
68
        }
69
    }
70
71
    private function applyDecorate(
72
        ContainerBuilder $container,
73
        string $decoratorServiceId,
74
        string $decoratedServiceId,
75
        array $parameters
76
    ): void {
77
        /** @var Definition $decoratorDefinition */
78
        $decoratorDefinition = $container->getDefinition($decoratorServiceId);
79
80
        /** @var array<string, mixed> $newDecoratorTags */
81
        $newDecoratorTags = $decoratorDefinition->getTags();
82
83
        unset($newDecoratorTags[$this->tagNameDecorates]);
84
85
        $decoratorDefinition->setTags($newDecoratorTags);
86
87
        /** @var string $newDecoratorServiceId */
88
        $newDecoratorServiceId = sprintf(
89
            "%s.%s.%s",
90
            $decoratedServiceId,
91
            $this->tagNameDecorates,
92
            $decoratorServiceId
93
        );
94
95
        /** @var Definition $newDecoratorDefinition */
96
        $newDecoratorDefinition = $this->filterDefinition(
97
            $decoratorDefinition,
98
            $parameters,
99
            $decoratorServiceId,
100
            $newDecoratorServiceId
101
        );
102
103
        $newDecoratorDefinition->setDecoratedService($decoratedServiceId);
104
105
        $container->setDefinition($newDecoratorServiceId, $newDecoratorDefinition);
106
    }
107
108
    private function filterDefinition(
109
        Definition $oldDefinition,
110
        array $parameters,
111
        string $decoratorServiceId,
112
        string $newDecoratorServiceId
113
    ): Definition {
114
        /** @var Definition $newDefinition */
115
        $newDefinition = clone $oldDefinition;
116
117
        /** @var array<int, Reference|array|string> $newArguments */
118
        $newArguments = array();
119
120
        foreach ($oldDefinition->getArguments() as $key => $oldArgument) {
121
            /** @var Reference|array|string $oldArgument */
122
123
            $newArguments[$key] = $this->filterArgument(
124
                $oldArgument,
125
                $parameters,
126
                $decoratorServiceId,
127
                $newDecoratorServiceId
128
            );
129
        }
130
131
        $newDefinition->setArguments($newArguments);
132
133
        return $newDefinition;
134
    }
135
136
    /**
137
     * @param Reference|array|string $oldArgument
138
     *
139
     * @return Reference|array|string
140
     */
141
    private function filterArgument(
142
        $oldArgument,
143
        array $parameters,
144
        string $decoratorServiceId,
145
        string $newDecoratorServiceId
146
    ) {
147
        /** @var Reference|array|string $newArgument */
148
        $newArgument = $oldArgument;
149
150
        if (is_string($oldArgument)) {
151
            $newArgument = $this->filterStringArgument(
152
                $oldArgument,
153
                $parameters,
154
                $decoratorServiceId,
155
                $newDecoratorServiceId
156
            );
157
        }
158
159
        if ($oldArgument instanceof Reference) {
160
            /** @var string $newReferenceId */
161
            $newReferenceId = $this->filterStringArgument(
162
                $oldArgument->__toString(),
163
                $parameters,
164
                $decoratorServiceId,
165
                $newDecoratorServiceId
166
            );
167
168
            $newArgument = new Reference(
169
                $newReferenceId,
170
                $oldArgument->getInvalidBehavior()
171
            );
172
        }
173
174
        if (is_array($newArgument)) {
175
            foreach ($newArgument as &$value) {
176
                $value = $this->filterArgument(
177
                    $value,
178
                    $parameters,
179
                    $decoratorServiceId,
180
                    $newDecoratorServiceId
181
                );
182
            }
183
        }
184
185
        return $newArgument;
186
    }
187
188
    private function filterStringArgument(
189
        string $oldArgument,
190
        array $parameters,
191
        string $decoratorServiceId,
192
        string $newDecoratorServiceId
193
    ): string {
194
        /** @var string $newArgument */
195
        $newArgument = $oldArgument;
196
197
        if ($oldArgument === $decoratorServiceId . ".inner") {
198
            $newArgument = $newDecoratorServiceId . ".inner";
199
200
        } elseif (preg_match("/^\%([a-zA-Z0-9_]+)\%$/is", $oldArgument, $matches)) {
201
            /** @var string $parameterName */
202
            $parameterName = $matches[1];
203
204
            if (isset($parameters[$parameterName])) {
205
                $newArgument = $parameters[$parameterName];
206
            }
207
        }
208
209
        return $newArgument;
210
    }
211
212
}
213