[BUGFIX] Fix several typos in php comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Object / Container / Container.php
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 Psr\Log\LoggerInterface;
24 use TYPO3\CMS\Core\SingletonInterface;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Extbase\Reflection\ClassSchema;
27 use TYPO3\CMS\Extbase\Reflection\ReflectionService;
28
29 /**
30 * Internal TYPO3 Dependency Injection container
31 * @internal only to be used within Extbase, not part of TYPO3 Core API.
32 */
33 class Container implements SingletonInterface, LoggerAwareInterface
34 {
35 use LoggerAwareTrait;
36
37 const SCOPE_PROTOTYPE = 1;
38 const SCOPE_SINGLETON = 2;
39
40 /**
41 * @var ContainerInterface
42 */
43 private $psrContainer;
44
45 /**
46 * registered alternative implementations of a class
47 * e.g. used to know the class for an AbstractClass or a Dependency
48 *
49 * @var array
50 */
51 private $alternativeImplementation;
52
53 /**
54 * @var InstantiatorInterface
55 */
56 protected $instantiator;
57
58 /**
59 * holds references of singletons
60 *
61 * @var array
62 */
63 private $singletonInstances = [];
64
65 /**
66 * Array of prototype objects currently being built, to prevent recursion.
67 *
68 * @var array
69 */
70 private $prototypeObjectsWhichAreCurrentlyInstanciated;
71
72 /**
73 * @var ReflectionService
74 */
75 private $reflectionService;
76
77 /**
78 * @param ContainerInterface $psrContainer
79 */
80 public function __construct(ContainerInterface $psrContainer)
81 {
82 $this->psrContainer = $psrContainer;
83 }
84
85 /**
86 * Internal method to create the class instantiator, extracted to be mockable
87 *
88 * @return InstantiatorInterface
89 */
90 protected function getInstantiator(): InstantiatorInterface
91 {
92 if ($this->instantiator == null) {
93 $this->instantiator = new \Doctrine\Instantiator\Instantiator();
94 }
95 return $this->instantiator;
96 }
97
98 /**
99 * Main method which should be used to get an instance of the wished class
100 * specified by $className.
101 *
102 * @param string $className
103 * @param array $givenConstructorArguments the list of constructor arguments as array
104 * @return object the built object
105 * @internal
106 */
107 public function getInstance(string $className, array $givenConstructorArguments = [])
108 {
109 $this->prototypeObjectsWhichAreCurrentlyInstanciated = [];
110 return $this->getInstanceInternal($className, ...$givenConstructorArguments);
111 }
112
113 /**
114 * Create an instance of $className without calling its constructor
115 *
116 * @param string $className
117 * @return object
118 */
119 public function getEmptyObject(string $className): object
120 {
121 $className = $this->getImplementationClassName($className);
122 $classSchema = $this->getReflectionService()->getClassSchema($className);
123 $object = $this->getInstantiator()->instantiate($className);
124 $this->injectDependencies($object, $classSchema);
125 $this->initializeObject($object);
126 return $object;
127 }
128
129 /**
130 * Internal implementation for getting a class.
131 *
132 * @param string $className
133 * @param array $givenConstructorArguments the list of constructor arguments as array
134 * @throws \TYPO3\CMS\Extbase\Object\Exception
135 * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
136 * @return object the built object
137 */
138 protected function getInstanceInternal(string $className, ...$givenConstructorArguments): object
139 {
140 $className = $this->getImplementationClassName($className);
141
142 if ($givenConstructorArguments === [] && $this->psrContainer->has($className)) {
143 $instance = $this->psrContainer->get($className);
144 if (!is_object($instance)) {
145 throw new \TYPO3\CMS\Extbase\Object\Exception('PSR-11 container returned non object for class name "' . $className . '".', 1562240407);
146 }
147 return $instance;
148 }
149
150 $className = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($className);
151 if (isset($this->singletonInstances[$className])) {
152 if (!empty($givenConstructorArguments)) {
153 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
154 }
155 return $this->singletonInstances[$className];
156 }
157
158 $classSchema = $this->getReflectionService()->getClassSchema($className);
159 $classIsSingleton = $classSchema->isSingleton();
160 if (!$classIsSingleton) {
161 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== false) {
162 throw new \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
163 }
164 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = true;
165 }
166 $instance = $this->instanciateObject($classSchema, ...$givenConstructorArguments);
167 $this->injectDependencies($instance, $classSchema);
168 $this->initializeObject($instance);
169 if (!$classIsSingleton) {
170 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
171 }
172 return $instance;
173 }
174
175 /**
176 * Instantiates an object, possibly setting the constructor dependencies.
177 * Additionally, directly registers all singletons in the singleton registry,
178 * such that circular references of singletons are correctly instantiated.
179 *
180 * @param ClassSchema $classSchema
181 * @param array $givenConstructorArguments
182 * @throws \TYPO3\CMS\Extbase\Object\Exception
183 * @return object the new instance
184 */
185 protected function instanciateObject(ClassSchema $classSchema, ...$givenConstructorArguments): object
186 {
187 $className = $classSchema->getClassName();
188 $classIsSingleton = $classSchema->isSingleton();
189 if ($classIsSingleton && !empty($givenConstructorArguments)) {
190 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
191 }
192 $constructorArguments = $this->getConstructorArguments($className, $classSchema, $givenConstructorArguments);
193 $instance = GeneralUtility::makeInstance($className, ...$constructorArguments);
194 if ($classIsSingleton) {
195 $this->singletonInstances[$className] = $instance;
196 }
197 return $instance;
198 }
199
200 /**
201 * Inject setter-dependencies into $instance
202 *
203 * @param object $instance
204 * @param ClassSchema $classSchema
205 */
206 protected function injectDependencies(object $instance, ClassSchema $classSchema): void
207 {
208 if (!$classSchema->hasInjectMethods() && !$classSchema->hasInjectProperties()) {
209 return;
210 }
211 foreach ($classSchema->getInjectMethods() as $injectMethodName => $injectMethod) {
212 $instanceToInject = $this->getInstanceInternal($injectMethod->getFirstParameter()->getDependency());
213 if ($classSchema->isSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
214 $this->getLogger()->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.');
215 }
216 if (is_callable([$instance, $injectMethodName])) {
217 $instance->{$injectMethodName}($instanceToInject);
218 }
219 }
220 foreach ($classSchema->getInjectProperties() as $injectPropertyName => $injectProperty) {
221 $classNameToInject = $injectProperty->getType();
222
223 $instanceToInject = $this->getInstanceInternal($classNameToInject);
224 if ($classSchema->isSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
225 $this->getLogger()->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.');
226 }
227
228 if ($classSchema->getProperty($injectPropertyName)->isPublic()) {
229 $instance->{$injectPropertyName} = $instanceToInject;
230 } else {
231 $propertyReflection = new \ReflectionProperty($instance, $injectPropertyName);
232 $propertyReflection->setAccessible(true);
233 $propertyReflection->setValue($instance, $instanceToInject);
234 }
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 string $className
267 * @param ClassSchema $classSchema
268 * @param array $givenConstructorArguments
269 * @throws \InvalidArgumentException
270 * @return array
271 */
272 private function getConstructorArguments(string $className, ClassSchema $classSchema, array $givenConstructorArguments): array
273 {
274 // @todo: drop the $className argument here.
275 // @todo: 1) we have that via the $classSchema object
276 // @todo: 2) if we drop the log message, the argument is superfluous
277 //
278 // @todo: -> private function getConstructorArguments(Method $constructor, array $givenConstructorArguments)
279
280 if (!$classSchema->hasConstructor()) {
281 // todo: this check needs to take place outside this method
282 // todo: Instead of passing a ClassSchema object in here, all we need is a Method object instead
283 return [];
284 }
285
286 $arguments = [];
287 foreach ($classSchema->getMethod('__construct')->getParameters() as $methodParameter) {
288 $index = $methodParameter->getPosition();
289
290 // Constructor argument given AND argument is a simple type OR instance of argument type
291 if (array_key_exists($index, $givenConstructorArguments) &&
292 ($methodParameter->getDependency() === null || is_a($givenConstructorArguments[$index], $methodParameter->getDependency()))
293 ) {
294 $argument = $givenConstructorArguments[$index];
295 } else {
296 if ($methodParameter->getDependency() !== null && !$methodParameter->hasDefaultValue()) {
297 $argument = $this->getInstanceInternal($methodParameter->getDependency());
298 if ($classSchema->isSingleton() && !$argument instanceof \TYPO3\CMS\Core\SingletonInterface) {
299 $this->getLogger()->notice('The singleton "' . $className . '" needs a prototype in the constructor. This is often a bad code smell; often you rather want to inject a singleton.');
300 // todo: the whole injection is flawed anyway, why would we care about injecting prototypes? so, wayne?
301 // todo: btw: if this is important, we can already detect this case in the class schema.
302 }
303 } elseif ($methodParameter->hasDefaultValue() === true) {
304 $argument = $methodParameter->getDefaultValue();
305 } else {
306 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!', 1476107941);
307 }
308 }
309 $arguments[] = $argument;
310 }
311 return $arguments;
312 }
313
314 /**
315 * Returns the class name for a new instance, taking into account the
316 * class-extension API.
317 *
318 * @param string $className Base class name to evaluate
319 * @return string Final class name to instantiate with "new [classname]
320 */
321 public function getImplementationClassName(string $className): string
322 {
323 if (isset($this->alternativeImplementation[$className])) {
324 $className = $this->alternativeImplementation[$className];
325 }
326 if (substr($className, -9) === 'Interface') {
327 $className = substr($className, 0, -9);
328 }
329 return $className;
330 }
331
332 /**
333 * @param string $className
334 *
335 * @return bool
336 */
337 public function isSingleton(string $className): bool
338 {
339 return $this->getReflectionService()->getClassSchema($className)->isSingleton();
340 }
341
342 /**
343 * @param string $className
344 *
345 * @return bool
346 */
347 public function isPrototype(string $className): bool
348 {
349 return !$this->isSingleton($className);
350 }
351
352 /**
353 * @return LoggerInterface
354 */
355 protected function getLogger(): LoggerInterface
356 {
357 return $this->logger;
358 }
359
360 /**
361 * Lazy load ReflectionService.
362 *
363 * Required as this class is being loaded in ext_localconf.php and we MUST not
364 * create caches in ext_localconf.php (which ReflectionService needs to do).
365 *
366 * @return ReflectionService
367 */
368 protected function getReflectionService(): ReflectionService
369 {
370 return $this->reflectionService ?? ($this->reflectionService = $this->psrContainer->get(ReflectionService::class));
371 }
372 }