Completed
Push — master ( 5ae395...f2deb1 )
by
unknown
14:39
created

Container::isPrototype()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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