Completed
Push — 3.x ( fd3361...37d5d4 )
by Grégoire
04:42
created

ExtensionCompilerPass   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 4
dl 0
loc 207
rs 9.0399
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
F process() 0 62 15
F getExtensionsForAdmin() 0 55 16
A getManagedClass() 0 4 1
A flattenExtensionConfiguration() 0 26 5
A hasTrait() 0 12 3
A addExtension() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like ExtensionCompilerPass often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExtensionCompilerPass, and based on these observations, apply Extract Interface, too.

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
 * @author Thomas Rabaix <[email protected]>
23
 */
24
class ExtensionCompilerPass implements CompilerPassInterface
25
{
26
    public function process(ContainerBuilder $container)
27
    {
28
        $universalExtensions = [];
29
        $targets = [];
30
31
        foreach ($container->findTaggedServiceIds('sonata.admin.extension') as $id => $tags) {
32
            foreach ($tags as $attributes) {
33
                $target = false;
34
35
                if (isset($attributes['target'])) {
36
                    $target = $attributes['target'];
37
                }
38
39
                if (isset($attributes['global']) && $attributes['global']) {
40
                    $universalExtensions[$id] = $attributes;
41
                }
42
43
                if (!$target || !$container->hasDefinition($target)) {
44
                    continue;
45
                }
46
47
                $this->addExtension($targets, $target, $id, $attributes);
48
            }
49
        }
50
51
        $extensionConfig = $container->getParameter('sonata.admin.extension.map');
52
        $extensionMap = $this->flattenExtensionConfiguration($extensionConfig);
53
54
        foreach ($container->findTaggedServiceIds('sonata.admin') as $id => $attributes) {
55
            $admin = $container->getDefinition($id);
56
57
            if (!isset($targets[$id])) {
58
                $targets[$id] = new \SplPriorityQueue();
59
            }
60
61
            foreach ($universalExtensions as $extension => $extensionAttributes) {
62
                $this->addExtension($targets, $id, $extension, $extensionAttributes);
63
            }
64
65
            $extensions = $this->getExtensionsForAdmin($id, $admin, $container, $extensionMap);
66
67
            foreach ($extensions as $extension => $attributes) {
68
                if (!$container->has($extension)) {
69
                    throw new \InvalidArgumentException(
70
                        sprintf('Unable to find extension service for id %s', $extension)
71
                    );
72
                }
73
74
                $this->addExtension($targets, $id, $extension, $attributes);
75
            }
76
        }
77
78
        foreach ($targets as $target => $extensions) {
79
            $extensions = iterator_to_array($extensions);
80
            krsort($extensions);
81
            $admin = $container->getDefinition($target);
82
83
            foreach (array_values($extensions) as $extension) {
84
                $admin->addMethodCall('addExtension', [$extension]);
85
            }
86
        }
87
    }
88
89
    /**
90
     * @param string $id
91
     *
92
     * @return array
93
     */
94
    protected function getExtensionsForAdmin($id, Definition $admin, ContainerBuilder $container, array $extensionMap)
95
    {
96
        $extensions = [];
97
        $classReflection = $subjectReflection = null;
98
99
        $excludes = $extensionMap['excludes'];
100
        unset($extensionMap['excludes']);
101
102
        foreach ($extensionMap as $type => $subjects) {
103
            foreach ($subjects as $subject => $extensionList) {
104
                if ('admins' == $type) {
105
                    if ($id == $subject) {
106
                        $extensions = array_merge($extensions, $extensionList);
107
                    }
108
                } else {
109
                    $class = $this->getManagedClass($admin, $container);
110
                    if (!class_exists($class)) {
111
                        continue;
112
                    }
113
                    $classReflection = new \ReflectionClass($class);
114
                    $subjectReflection = new \ReflectionClass($subject);
115
                }
116
117
                if ('instanceof' == $type) {
118
                    if ($subjectReflection->getName() == $classReflection->getName() || $classReflection->isSubclassOf($subject)) {
119
                        $extensions = array_merge($extensions, $extensionList);
120
                    }
121
                }
122
123
                if ('implements' == $type) {
124
                    if ($classReflection->implementsInterface($subject)) {
125
                        $extensions = array_merge($extensions, $extensionList);
126
                    }
127
                }
128
129
                if ('extends' == $type) {
130
                    if ($classReflection->isSubclassOf($subject)) {
131
                        $extensions = array_merge($extensions, $extensionList);
132
                    }
133
                }
134
135
                if ('uses' == $type) {
136
                    if ($this->hasTrait($classReflection, $subject)) {
0 ignored issues
show
Bug introduced by
It seems like $classReflection defined by $subjectReflection = null on line 97 can be null; however, Sonata\AdminBundle\Depen...ompilerPass::hasTrait() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
137
                        $extensions = array_merge($extensions, $extensionList);
138
                    }
139
                }
140
            }
141
        }
142
143
        if (isset($excludes[$id])) {
144
            $extensions = array_diff_key($extensions, $excludes[$id]);
145
        }
146
147
        return $extensions;
148
    }
149
150
    /**
151
     * Resolves the class argument of the admin to an actual class (in case of %parameter%).
152
     *
153
     * @return string
154
     */
155
    protected function getManagedClass(Definition $admin, ContainerBuilder $container)
156
    {
157
        return $container->getParameterBag()->resolveValue($admin->getArgument(1));
158
    }
159
160
    /**
161
     * @return array an array with the following structure.
162
     *
163
     * [
164
     *     'excludes'   => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
165
     *     'admins'     => ['<admin_id>'  => ['<extension_id>' => ['priority' => <int>]]],
166
     *     'implements' => ['<interface>' => ['<extension_id>' => ['priority' => <int>]]],
167
     *     'extends'    => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
168
     *     'instanceof' => ['<class>'     => ['<extension_id>' => ['priority' => <int>]]],
169
     *     'uses'       => ['<trait>'     => ['<extension_id>' => ['priority' => <int>]]],
170
     * ]
171
     */
172
    protected function flattenExtensionConfiguration(array $config)
173
    {
174
        $extensionMap = [
175
            'excludes' => [],
176
            'admins' => [],
177
            'implements' => [],
178
            'extends' => [],
179
            'instanceof' => [],
180
            'uses' => [],
181
        ];
182
183
        foreach ($config as $extension => $options) {
184
            $optionsMap = array_intersect_key($options, $extensionMap);
185
186
            foreach ($optionsMap as $key => $value) {
187
                foreach ($value as $source) {
188
                    if (!isset($extensionMap[$key][$source])) {
189
                        $extensionMap[$key][$source] = [];
190
                    }
191
                    $extensionMap[$key][$source][$extension]['priority'] = $options['priority'];
192
                }
193
            }
194
        }
195
196
        return $extensionMap;
197
    }
198
199
    /**
200
     * @return bool
201
     */
202
    protected function hasTrait(\ReflectionClass $class, $traitName)
203
    {
204
        if (\in_array($traitName, $class->getTraitNames())) {
205
            return true;
206
        }
207
208
        if (!$parentClass = $class->getParentClass()) {
209
            return false;
210
        }
211
212
        return $this->hasTrait($parentClass, $traitName);
213
    }
214
215
    /**
216
     * Add extension configuration to the targets array.
217
     *
218
     * @param string $target
219
     * @param string $extension
220
     */
221
    private function addExtension(array &$targets, $target, $extension, array $attributes)
222
    {
223
        if (!isset($targets[$target])) {
224
            $targets[$target] = new \SplPriorityQueue();
225
        }
226
227
        $priority = $attributes['priority'] ?? 0;
228
        $targets[$target]->insert(new Reference($extension), $priority);
229
    }
230
}
231