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