Completed
Pull Request — 3.x (#6028)
by Christian
03:25
created

ExtensionCompilerPass::getManagedClass()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 4
nc 3
nop 2
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(
72
                        sprintf('Unable to find extension service for id %s', $extension)
73
                    );
74
                }
75
76
                $this->addExtension($targets, $id, $extension, $attributes);
77
            }
78
        }
79
80
        foreach ($targets as $target => $extensions) {
81
            $extensions = iterator_to_array($extensions);
82
            krsort($extensions);
83
            $admin = $container->getDefinition($target);
84
85
            foreach (array_values($extensions) as $extension) {
86
                $admin->addMethodCall('addExtension', [$extension]);
87
            }
88
        }
89
    }
90
91
    /**
92
     * @param string $id
93
     *
94
     * @return array
95
     */
96
    protected function getExtensionsForAdmin($id, Definition $admin, ContainerBuilder $container, array $extensionMap)
97
    {
98
        $extensions = [];
99
100
        $excludes = $extensionMap['excludes'];
101
        unset($extensionMap['excludes']);
102
103
        foreach ($extensionMap as $type => $subjects) {
104
            foreach ($subjects as $subject => $extensionList) {
105
                $class = null;
106
107
                if ('admins' === $type) {
108
                    if ($id === $subject) {
109
                        $extensions = array_merge($extensions, $extensionList);
110
                    }
111
                } else {
112
                    $class = $this->getManagedClass($admin, $container);
113
                }
114
115
                if (null === $class || !class_exists($class)) {
116
                    continue;
117
                }
118
119
                if ($this->isExtensionClass($type, $class, $subject)) {
120
                    $extensions = array_merge($extensions, $extensionList);
121
                }
122
            }
123
        }
124
125
        if (isset($excludes[$id])) {
126
            $extensions = array_diff_key($extensions, $excludes[$id]);
127
        }
128
129
        return $extensions;
130
    }
131
132
    /**
133
     * Resolves the class argument of the admin to an actual class (in case of %parameter%).
134
     *
135
     * @return string
136
     */
137
    protected function getManagedClass(Definition $admin, ContainerBuilder $container)
138
    {
139
        $argument = $admin->getArgument(1);
140
        $class = $container->getParameterBag()->resolveValue($argument);
141
142
        if (null === $class) {
143
            throw new \DomainException(sprintf('The admin "%s" does not have a valid manager', $admin->getClass()));
144
        }
145
146
        if (!\is_string($class)) {
147
            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...
148
                'Argument "%s" for admin class "%s" must be of type string, %s given.',
149
                $argument,
150
                $admin->getClass(),
151
                \is_object($class) ? \get_class($class) : \gettype($class)
152
            ));
153
        }
154
155
        return $class;
156
    }
157
158
    /**
159
     * @return array an array with the following structure.
160
     *
161
     * [
162
     *     'excludes'   => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
163
     *     'admins'     => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
164
     *     'implements' => ['<interface>' => ['<extension_id>' => ['priority' => <int>]]],
165
     *     'extends'    => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
166
     *     'instanceof' => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
167
     *     'uses'       => ['<trait>'     => ['<extension_id>' => ['priority' => <int>]]],
168
     * ]
169
     */
170
    protected function flattenExtensionConfiguration(array $config)
171
    {
172
        $extensionMap = [
173
            'excludes' => [],
174
            'admins' => [],
175
            'implements' => [],
176
            'extends' => [],
177
            'instanceof' => [],
178
            'uses' => [],
179
        ];
180
181
        foreach ($config as $extension => $options) {
182
            $optionsMap = array_intersect_key($options, $extensionMap);
183
184
            foreach ($optionsMap as $key => $value) {
185
                foreach ($value as $source) {
186
                    if (!isset($extensionMap[$key][$source])) {
187
                        $extensionMap[$key][$source] = [];
188
                    }
189
                    $extensionMap[$key][$source][$extension]['priority'] = $options['priority'];
190
                }
191
            }
192
        }
193
194
        return $extensionMap;
195
    }
196
197
    /**
198
     * @return bool
199
     */
200
    protected function hasTrait(\ReflectionClass $class, $traitName)
201
    {
202
        if (\in_array($traitName, $class->getTraitNames(), true)) {
203
            return true;
204
        }
205
206
        if (!$parentClass = $class->getParentClass()) {
207
            return false;
208
        }
209
210
        return $this->hasTrait($parentClass, $traitName);
211
    }
212
213
    private function isExtensionClass(string $type, string $class, string $subject): bool
214
    {
215
        $classReflection = new \ReflectionClass($class);
216
        $subjectReflection = new \ReflectionClass($subject);
217
218
        switch ($type) {
219
            case 'instanceof':
220
                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...
221
            case 'implements':
222
                return $classReflection->implementsInterface($subject);
223
            case 'extends':
224
                return $classReflection->isSubclassOf($subject);
225
            case 'uses':
226
                return $this->hasTrait($classReflection, $subject);
227
        }
228
229
        return false;
230
    }
231
232
    /**
233
     * Add extension configuration to the targets array.
234
     */
235
    private function addExtension(
236
        array &$targets,
237
        string $target,
238
        string $extension,
239
        array $attributes
240
    ): void {
241
        if (!isset($targets[$target])) {
242
            $targets[$target] = new \SplPriorityQueue();
243
        }
244
245
        $priority = $attributes['priority'] ?? 0;
246
        $targets[$target]->insert(new Reference($extension), $priority);
247
    }
248
}
249