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

ExtensionCompilerPass::isExtensionClass()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.0444
c 0
b 0
f 0
cc 6
nc 6
nop 3
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
                if ('admins' === $type) {
107
                    if ($id === $subject) {
108
                        $extensions = array_merge($extensions, $extensionList);
109
                    }
110
111
                    continue;
112
                }
113
114
                $class = $this->getManagedClass($admin, $container);
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|null
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
            // NEXT_MAJOR: Throw exception
145
//            throw new \DomainException(sprintf('The admin "%s" does not have a valid manager.', $admin->getClass()));
146
147
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
148
                sprintf('The admin "%s" does not have a valid manager.', $admin->getClass()),
149
                E_USER_DEPRECATED
150
            );
151
        }
152
153
        if (!\is_string($class)) {
154
            // NEXT_MAJOR: Throw exception
155
//            throw new \TypeError(sprintf(
156
//                'Argument "%s" for admin class "%s" must be of type string, %s given.',
157
//                $argument,
158
//                $admin->getClass(),
159
//                \is_object($class) ? \get_class($class) : \gettype($class)
160
//            ));
161
162
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
163
                sprintf(
164
                    'Argument "%s" for admin class "%s" must be of type string, %s given.',
165
                    $argument,
166
                    $admin->getClass(),
167
                    \is_object($class) ? \get_class($class) : \gettype($class)
168
                ),
169
                E_USER_DEPRECATED
170
            );
171
        }
172
173
        return $class;
174
    }
175
176
    /**
177
     * @return array an array with the following structure.
178
     *
179
     * [
180
     *     'excludes'   => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
181
     *     'admins'     => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
182
     *     'implements' => ['<interface>' => ['<extension_id>' => ['priority' => <int>]]],
183
     *     'extends'    => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
184
     *     'instanceof' => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
185
     *     'uses'       => ['<trait>'     => ['<extension_id>' => ['priority' => <int>]]],
186
     * ]
187
     */
188
    protected function flattenExtensionConfiguration(array $config)
189
    {
190
        $extensionMap = [
191
            'excludes' => [],
192
            'admins' => [],
193
            'implements' => [],
194
            'extends' => [],
195
            'instanceof' => [],
196
            'uses' => [],
197
        ];
198
199
        foreach ($config as $extension => $options) {
200
            $optionsMap = array_intersect_key($options, $extensionMap);
201
202
            foreach ($optionsMap as $key => $value) {
203
                foreach ($value as $source) {
204
                    if (!isset($extensionMap[$key][$source])) {
205
                        $extensionMap[$key][$source] = [];
206
                    }
207
                    $extensionMap[$key][$source][$extension]['priority'] = $options['priority'];
208
                }
209
            }
210
        }
211
212
        return $extensionMap;
213
    }
214
215
    /**
216
     * @return bool
217
     */
218
    protected function hasTrait(\ReflectionClass $class, $traitName)
219
    {
220
        if (\in_array($traitName, $class->getTraitNames(), true)) {
221
            return true;
222
        }
223
224
        if (!$parentClass = $class->getParentClass()) {
225
            return false;
226
        }
227
228
        return $this->hasTrait($parentClass, $traitName);
229
    }
230
231
    private function isExtensionClass(string $type, string $class, string $subject): bool
232
    {
233
        $classReflection = new \ReflectionClass($class);
234
        $subjectReflection = new \ReflectionClass($subject);
235
236
        switch ($type) {
237
            case 'instanceof':
238
                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...
239
            case 'implements':
240
                return $classReflection->implementsInterface($subject);
241
            case 'extends':
242
                return $classReflection->isSubclassOf($subject);
243
            case 'uses':
244
                return $this->hasTrait($classReflection, $subject);
245
        }
246
247
        return false;
248
    }
249
250
    /**
251
     * Add extension configuration to the targets array.
252
     */
253
    private function addExtension(
254
        array &$targets,
255
        string $target,
256
        string $extension,
257
        array $attributes
258
    ): void {
259
        if (!isset($targets[$target])) {
260
            $targets[$target] = new \SplPriorityQueue();
261
        }
262
263
        $priority = $attributes['priority'] ?? 0;
264
        $targets[$target]->insert(new Reference($extension), $priority);
265
    }
266
}
267