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