Completed
Pull Request — 3.x (#6028)
by Christian
02:54
created

ExtensionCompilerPass::getExtensionsForAdmin()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.0555
c 0
b 0
f 0
cc 9
nc 22
nop 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\DependencyInjection\Compiler;
15
16
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17
use Symfony\Component\DependencyInjection\ContainerBuilder;
18
use Symfony\Component\DependencyInjection\Definition;
19
use Symfony\Component\DependencyInjection\Reference;
20
21
/**
22
 * @final since sonata-project/admin-bundle 3.52
23
 *
24
 * @author Thomas Rabaix <[email protected]>
25
 */
26
class ExtensionCompilerPass implements CompilerPassInterface
27
{
28
    public function process(ContainerBuilder $container)
29
    {
30
        $universalExtensions = [];
31
        $targets = [];
32
33
        foreach ($container->findTaggedServiceIds('sonata.admin.extension') as $id => $tags) {
34
            foreach ($tags as $attributes) {
35
                $target = false;
36
37
                if (isset($attributes['target'])) {
38
                    $target = $attributes['target'];
39
                }
40
41
                if (isset($attributes['global']) && $attributes['global']) {
42
                    $universalExtensions[$id] = $attributes;
43
                }
44
45
                if (!$target || !$container->hasDefinition($target)) {
46
                    continue;
47
                }
48
49
                $this->addExtension($targets, $target, $id, $attributes);
50
            }
51
        }
52
53
        $extensionConfig = $container->getParameter('sonata.admin.extension.map');
54
        $extensionMap = $this->flattenExtensionConfiguration($extensionConfig);
55
56
        foreach ($container->findTaggedServiceIds('sonata.admin') as $id => $attributes) {
57
            $admin = $container->getDefinition($id);
58
59
            if (!isset($targets[$id])) {
60
                $targets[$id] = new \SplPriorityQueue();
61
            }
62
63
            foreach ($universalExtensions as $extension => $extensionAttributes) {
64
                $this->addExtension($targets, $id, $extension, $extensionAttributes);
65
            }
66
67
            $extensions = $this->getExtensionsForAdmin($id, $admin, $container, $extensionMap);
68
69
            foreach ($extensions as $extension => $attributes) {
70
                if (!$container->has($extension)) {
71
                    throw new \InvalidArgumentException(sprintf(
72
                        'Unable to find extension service for id %s',
73
                        $extension
74
                    ));
75
                }
76
77
                $this->addExtension($targets, $id, $extension, $attributes);
78
            }
79
        }
80
81
        foreach ($targets as $target => $extensions) {
82
            $extensions = iterator_to_array($extensions);
83
            krsort($extensions);
84
            $admin = $container->getDefinition($target);
85
86
            foreach (array_values($extensions) as $extension) {
87
                $admin->addMethodCall('addExtension', [$extension]);
88
            }
89
        }
90
    }
91
92
    /**
93
     * @param string $id
94
     *
95
     * @return array
96
     */
97
    protected function getExtensionsForAdmin($id, Definition $admin, ContainerBuilder $container, array $extensionMap)
98
    {
99
        $extensions = [];
100
101
        $excludes = $extensionMap['excludes'];
102
        unset($extensionMap['excludes']);
103
104
        foreach ($extensionMap as $type => $subjects) {
105
            foreach ($subjects as $subject => $extensionList) {
106
                $class = null;
107
108
                if ('admins' === $type) {
109
                    if ($id === $subject) {
110
                        $extensions = array_merge($extensions, $extensionList);
111
                    }
112
                } else {
113
                    $class = $this->getManagedClass($admin, $container);
114
                }
115
116
                if (null === $class || !class_exists($class)) {
117
                    continue;
118
                }
119
120
                if ($this->isExtensionClass($type, $class, $subject)) {
121
                    $extensions = array_merge($extensions, $extensionList);
122
                }
123
            }
124
        }
125
126
        if (isset($excludes[$id])) {
127
            $extensions = array_diff_key($extensions, $excludes[$id]);
128
        }
129
130
        return $extensions;
131
    }
132
133
    /**
134
     * Resolves the class argument of the admin to an actual class (in case of %parameter%).
135
     *
136
     * @return string
137
     */
138
    protected function getManagedClass(Definition $admin, ContainerBuilder $container)
139
    {
140
        $argument = $admin->getArgument(1);
141
        $class = $container->getParameterBag()->resolveValue($argument);
142
143
        if (null === $class) {
144
            throw new \DomainException(sprintf('The admin "%s" does not have a valid manager.', $admin->getClass()));
145
        }
146
147
        if (!\is_string($class)) {
148
            throw new \TypeError(sprintf(
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('Argument "%s" f...ss) : \gettype($class)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
149
                'Argument "%s" for admin class "%s" must be of type string, %s given.',
150
                $argument,
151
                $admin->getClass(),
152
                \is_object($class) ? \get_class($class) : \gettype($class)
153
            ));
154
        }
155
156
        return $class;
157
    }
158
159
    /**
160
     * @return array an array with the following structure.
161
     *
162
     * [
163
     *     'excludes'   => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
164
     *     'admins'     => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
165
     *     'implements' => ['<interface>' => ['<extension_id>' => ['priority' => <int>]]],
166
     *     'extends'    => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
167
     *     'instanceof' => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
168
     *     'uses'       => ['<trait>'     => ['<extension_id>' => ['priority' => <int>]]],
169
     * ]
170
     */
171
    protected function flattenExtensionConfiguration(array $config)
172
    {
173
        $extensionMap = [
174
            'excludes' => [],
175
            'admins' => [],
176
            'implements' => [],
177
            'extends' => [],
178
            'instanceof' => [],
179
            'uses' => [],
180
        ];
181
182
        foreach ($config as $extension => $options) {
183
            $optionsMap = array_intersect_key($options, $extensionMap);
184
185
            foreach ($optionsMap as $key => $value) {
186
                foreach ($value as $source) {
187
                    if (!isset($extensionMap[$key][$source])) {
188
                        $extensionMap[$key][$source] = [];
189
                    }
190
                    $extensionMap[$key][$source][$extension]['priority'] = $options['priority'];
191
                }
192
            }
193
        }
194
195
        return $extensionMap;
196
    }
197
198
    /**
199
     * @return bool
200
     */
201
    protected function hasTrait(\ReflectionClass $class, $traitName)
202
    {
203
        if (\in_array($traitName, $class->getTraitNames(), true)) {
204
            return true;
205
        }
206
207
        if (!$parentClass = $class->getParentClass()) {
208
            return false;
209
        }
210
211
        return $this->hasTrait($parentClass, $traitName);
212
    }
213
214
    private function isExtensionClass(string $type, string $class, string $subject): bool
215
    {
216
        $classReflection = new \ReflectionClass($class);
217
        $subjectReflection = new \ReflectionClass($subject);
218
219
        switch ($type) {
220
            case 'instanceof':
221
                return $classReflection->isSubclassOf($subject) || $subjectReflection->getName() === $classReflection->getName();
0 ignored issues
show
Bug introduced by
Consider using $subjectReflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
222
            case 'implements':
223
                return $classReflection->implementsInterface($subject);
224
            case 'extends':
225
                return $classReflection->isSubclassOf($subject);
226
            case 'uses':
227
                return $this->hasTrait($classReflection, $subject);
228
        }
229
230
        return false;
231
    }
232
233
    /**
234
     * Add extension configuration to the targets array.
235
     */
236
    private function addExtension(
237
        array &$targets,
238
        string $target,
239
        string $extension,
240
        array $attributes
241
    ): void {
242
        if (!isset($targets[$target])) {
243
            $targets[$target] = new \SplPriorityQueue();
244
        }
245
246
        $priority = $attributes['priority'] ?? 0;
247
        $targets[$target]->insert(new Reference($extension), $priority);
248
    }
249
}
250