Passed
Push — master ( 1fba08...8fefaa )
by
unknown
13:03
created

Container::injectDependencies()   C

Complexity

Conditions 12
Paths 25

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 17
nc 25
nop 2
dl 0
loc 28
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Extbase\Object\Container;
19
20
use Doctrine\Instantiator\Instantiator;
21
use Doctrine\Instantiator\InstantiatorInterface;
22
use Psr\Container\ContainerInterface;
23
use Psr\Log\LoggerAwareInterface;
24
use Psr\Log\LoggerAwareTrait;
25
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
26
use TYPO3\CMS\Core\SingletonInterface;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Extbase\Object\Exception;
29
use TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException;
30
use TYPO3\CMS\Extbase\Reflection\ClassSchema;
31
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
32
33
/**
34
 * Internal TYPO3 Dependency Injection container
35
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
36
 */
37
class Container implements SingletonInterface, LoggerAwareInterface
38
{
39
    use LoggerAwareTrait;
40
41
    /**
42
     * @var ContainerInterface
43
     */
44
    private $psrContainer;
45
46
    /**
47
     * registered alternative implementations of a class
48
     * e.g. used to know the class for an AbstractClass or a Dependency
49
     *
50
     * @var array
51
     */
52
    private $alternativeImplementation;
53
54
    /**
55
     * @var InstantiatorInterface
56
     */
57
    protected $instantiator;
58
59
    /**
60
     * holds references of singletons
61
     *
62
     * @var array
63
     */
64
    private $singletonInstances = [];
65
66
    /**
67
     * Array of prototype objects currently being built, to prevent recursion.
68
     *
69
     * @var array
70
     */
71
    private $prototypeObjectsWhichAreCurrentlyInstanciated;
72
73
    /**
74
     * @var ReflectionService
75
     */
76
    private $reflectionService;
77
78
    /**
79
     * @param ContainerInterface $psrContainer
80
     */
81
    public function __construct(ContainerInterface $psrContainer)
82
    {
83
        $this->psrContainer = $psrContainer;
84
    }
85
86
    /**
87
     * Internal method to create the class instantiator, extracted to be mockable
88
     *
89
     * @return InstantiatorInterface
90
     */
91
    protected function getInstantiator(): InstantiatorInterface
92
    {
93
        if ($this->instantiator == null) {
94
            $this->instantiator = new Instantiator();
95
        }
96
        return $this->instantiator;
97
    }
98
99
    /**
100
     * Main method which should be used to get an instance of the wished class
101
     * specified by $className.
102
     *
103
     * @param string $className
104
     * @param array $givenConstructorArguments the list of constructor arguments as array
105
     * @return object the built object
106
     * @internal
107
     */
108
    public function getInstance(string $className, array $givenConstructorArguments = [])
109
    {
110
        $this->prototypeObjectsWhichAreCurrentlyInstanciated = [];
111
        return $this->getInstanceInternal($className, ...$givenConstructorArguments);
112
    }
113
114
    /**
115
     * Create an instance of $className without calling its constructor
116
     *
117
     * @param string $className
118
     * @return object
119
     */
120
    public function getEmptyObject(string $className): object
121
    {
122
        $className = $this->getImplementationClassName($className);
123
        $classSchema = $this->getReflectionService()->getClassSchema($className);
124
        $object = $this->getInstantiator()->instantiate($className);
125
        $this->injectDependencies($object, $classSchema);
126
        $this->initializeObject($object);
127
        return $object;
128
    }
129
130
    /**
131
     * Internal implementation for getting a class.
132
     *
133
     * @param string $className
134
     * @param array<int,mixed> $givenConstructorArguments the list of constructor arguments as array
135
     * @throws \TYPO3\CMS\Extbase\Object\Exception
136
     * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
137
     * @return object the built object
138
     */
139
    protected function getInstanceInternal(string $className, ...$givenConstructorArguments): object
140
    {
141
        $className = $this->getImplementationClassName($className);
142
143
        if ($givenConstructorArguments === [] && $this->psrContainer->has($className)) {
144
            $instance = $this->psrContainer->get($className);
145
            if (!is_object($instance)) {
146
                throw new Exception('PSR-11 container returned non object for class name "' . $className . '".', 1562240407);
147
            }
148
            return $instance;
149
        }
150
151
        $className = ClassLoadingInformation::getClassNameForAlias($className);
152
        if (isset($this->singletonInstances[$className])) {
153
            if (!empty($givenConstructorArguments)) {
154
                throw new Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
155
            }
156
            return $this->singletonInstances[$className];
157
        }
158
159
        $classSchema = $this->getReflectionService()->getClassSchema($className);
160
        $classIsSingleton = $classSchema->isSingleton();
161
        if (!$classIsSingleton) {
162
            if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== false) {
163
                throw new CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
164
            }
165
            $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = true;
166
        }
167
        $instance = $this->instanciateObject($classSchema, ...$givenConstructorArguments);
168
        $this->injectDependencies($instance, $classSchema);
169
        $this->initializeObject($instance);
170
        if (!$classIsSingleton) {
171
            unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
172
        }
173
        return $instance;
174
    }
175
176
    /**
177
     * Instantiates an object, possibly setting the constructor dependencies.
178
     * Additionally, directly registers all singletons in the singleton registry,
179
     * such that circular references of singletons are correctly instantiated.
180
     *
181
     * @param ClassSchema $classSchema
182
     * @param array<int,mixed> $givenConstructorArguments
183
     * @throws \TYPO3\CMS\Extbase\Object\Exception
184
     * @return object the new instance
185
     */
186
    protected function instanciateObject(ClassSchema $classSchema, ...$givenConstructorArguments): object
187
    {
188
        $className = $classSchema->getClassName();
189
        $classIsSingleton = $classSchema->isSingleton();
190
        if ($classIsSingleton && !empty($givenConstructorArguments)) {
191
            throw new Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
192
        }
193
        $constructorArguments = $this->getConstructorArguments($classSchema, $givenConstructorArguments);
194
        $instance = GeneralUtility::makeInstance($className, ...$constructorArguments);
195
        if ($classIsSingleton) {
196
            $this->singletonInstances[$className] = $instance;
197
        }
198
        return $instance;
199
    }
200
201
    /**
202
     * Inject setter-dependencies into $instance
203
     *
204
     * @param object $instance
205
     * @param ClassSchema $classSchema
206
     */
207
    protected function injectDependencies(object $instance, ClassSchema $classSchema): void
208
    {
209
        if (!$classSchema->hasInjectMethods() && !$classSchema->hasInjectProperties()) {
210
            return;
211
        }
212
        foreach ($classSchema->getInjectMethods() as $injectMethodName => $injectMethod) {
213
            if (($classNameToInject = $injectMethod->getFirstParameter()->getDependency()) === null) {
214
                continue;
215
            }
216
            $instanceToInject = $this->getInstanceInternal($classNameToInject);
217
            if ($classSchema->isSingleton() && !$instanceToInject instanceof SingletonInterface) {
218
                $this->logger->notice('The singleton "' . $classSchema->getClassName() . '" needs a prototype in "' . $injectMethodName . '". This is often a bad code smell; often you rather want to inject a singleton.');
219
            }
220
            if (is_callable([$instance, $injectMethodName])) {
221
                $instance->{$injectMethodName}($instanceToInject);
222
            }
223
        }
224
        foreach ($classSchema->getInjectProperties() as $injectPropertyName => $injectProperty) {
225
            if (($classNameToInject = $injectProperty->getType()) === null) {
226
                continue;
227
            }
228
229
            $instanceToInject = $this->getInstanceInternal($classNameToInject);
230
            if ($classSchema->isSingleton() && !$instanceToInject instanceof SingletonInterface) {
231
                $this->logger->notice('The singleton "' . $classSchema->getClassName() . '" needs a prototype in "' . $injectPropertyName . '". This is often a bad code smell; often you rather want to inject a singleton.');
232
            }
233
234
            $instance->{$injectPropertyName} = $instanceToInject;
235
        }
236
    }
237
238
    /**
239
     * Call object initializer if present in object
240
     *
241
     * @param object $instance
242
     */
243
    protected function initializeObject(object $instance): void
244
    {
245
        if (method_exists($instance, 'initializeObject') && is_callable([$instance, 'initializeObject'])) {
246
            $instance->initializeObject();
247
        }
248
    }
249
250
    /**
251
     * register a classname that should be used if a dependency is required.
252
     * e.g. used to define default class for an interface
253
     *
254
     * @param string $className
255
     * @param string $alternativeClassName
256
     * @todo deprecate in favor of core DI configuration (aliases/overrides)
257
     */
258
    public function registerImplementation(string $className, string $alternativeClassName): void
259
    {
260
        $this->alternativeImplementation[$className] = $alternativeClassName;
261
    }
262
263
    /**
264
     * gets array of parameter that can be used to call a constructor
265
     *
266
     * @param ClassSchema $classSchema
267
     * @param array $givenConstructorArguments
268
     * @throws \InvalidArgumentException
269
     * @return array
270
     */
271
    private function getConstructorArguments(ClassSchema $classSchema, array $givenConstructorArguments): array
272
    {
273
        // @todo: -> private function getConstructorArguments(Method $constructor, array $givenConstructorArguments)
274
275
        if (!$classSchema->hasConstructor()) {
276
            // todo: this check needs to take place outside this method
277
            // todo: Instead of passing a ClassSchema object in here, all we need is a Method object instead
278
            return [];
279
        }
280
281
        $arguments = [];
282
        foreach ($classSchema->getMethod('__construct')->getParameters() as $methodParameter) {
283
            $index = $methodParameter->getPosition();
284
285
            // Constructor argument given AND argument is a simple type OR instance of argument type
286
            if (array_key_exists($index, $givenConstructorArguments) &&
287
                ($methodParameter->getDependency() === null || is_a($givenConstructorArguments[$index], $methodParameter->getDependency()))
288
            ) {
289
                $argument = $givenConstructorArguments[$index];
290
            } else {
291
                if ($methodParameter->getDependency() !== null && !$methodParameter->hasDefaultValue()) {
292
                    $argument = $this->getInstanceInternal($methodParameter->getDependency());
293
                    if ($classSchema->isSingleton() && !$argument instanceof SingletonInterface) {
294
                        $this->logger->notice('The singleton "' . $classSchema->getClassName() . '" needs a prototype in the constructor. This is often a bad code smell; often you rather want to inject a singleton.');
295
                        // todo: the whole injection is flawed anyway, why would we care about injecting prototypes? so, wayne?
296
                        // todo: btw: if this is important, we can already detect this case in the class schema.
297
                    }
298
                } elseif ($methodParameter->hasDefaultValue() === true) {
299
                    $argument = $methodParameter->getDefaultValue();
300
                } else {
301
                    throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!', 1476107941);
302
                }
303
            }
304
            $arguments[] = $argument;
305
        }
306
        return $arguments;
307
    }
308
309
    /**
310
     * Returns the class name for a new instance, taking into account the
311
     * class-extension API.
312
     *
313
     * @param string $className Base class name to evaluate
314
     * @return string Final class name to instantiate with "new [classname]
315
     */
316
    public function getImplementationClassName(string $className): string
317
    {
318
        if (isset($this->alternativeImplementation[$className])) {
319
            $className = $this->alternativeImplementation[$className];
320
        }
321
        if (substr($className, -9) === 'Interface') {
322
            $className = substr($className, 0, -9);
323
        }
324
        return $className;
325
    }
326
327
    /**
328
     * Lazy load ReflectionService.
329
     *
330
     * Required as this class is being loaded in ext_localconf.php and we MUST not
331
     * create caches in ext_localconf.php (which ReflectionService needs to do).
332
     *
333
     * @return ReflectionService
334
     */
335
    protected function getReflectionService(): ReflectionService
336
    {
337
        return $this->reflectionService ?? ($this->reflectionService = $this->psrContainer->get(ReflectionService::class));
338
    }
339
}
340