[TASK] Streamline phpdoc annotations in EXT:extbase
[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 => $classNameToInject) {
192 $instanceToInject = $this->getInstanceInternal($classNameToInject);
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 foreach ($classSchema->getInjectProperties() as $injectPropertyName => $classNameToInject) {
201 $instanceToInject = $this->getInstanceInternal($classNameToInject);
202 if ($classSchema->isSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
203 $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.');
204 }
205
206 if ($classSchema->getProperty($injectPropertyName)['public']) {
207 $instance->{$injectPropertyName} = $instanceToInject;
208 } else {
209 $propertyReflection = new \ReflectionProperty($instance, $injectPropertyName);
210 $propertyReflection->setAccessible(true);
211 $propertyReflection->setValue($instance, $instanceToInject);
212 }
213 }
214 }
215
216 /**
217 * Call object initializer if present in object
218 *
219 * @param object $instance
220 */
221 protected function initializeObject($instance)
222 {
223 if (method_exists($instance, 'initializeObject') && is_callable([$instance, 'initializeObject'])) {
224 $instance->initializeObject();
225 }
226 }
227
228 /**
229 * register a classname that should be used if a dependency is required.
230 * e.g. used to define default class for an interface
231 *
232 * @param string $className
233 * @param string $alternativeClassName
234 */
235 public function registerImplementation($className, $alternativeClassName)
236 {
237 $this->alternativeImplementation[$className] = $alternativeClassName;
238 }
239
240 /**
241 * gets array of parameter that can be used to call a constructor
242 *
243 * @param string $className
244 * @param ClassSchema $classSchema
245 * @param array $givenConstructorArguments
246 * @throws \InvalidArgumentException
247 * @return array
248 */
249 private function getConstructorArguments($className, ClassSchema $classSchema, array $givenConstructorArguments)
250 {
251 $parameters = [];
252 $constructorArgumentInformation = $classSchema->getConstructorArguments();
253 foreach ($constructorArgumentInformation as $constructorArgumentName => $argumentInformation) {
254 $index = $argumentInformation['position'];
255
256 // Constructor argument given AND argument is a simple type OR instance of argument type
257 if (array_key_exists($index, $givenConstructorArguments) && (!isset($argumentInformation['dependency']) || is_a($givenConstructorArguments[$index], $argumentInformation['dependency']))) {
258 $parameter = $givenConstructorArguments[$index];
259 } else {
260 if (isset($argumentInformation['dependency']) && $argumentInformation['hasDefaultValue'] === false) {
261 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
262 if ($classSchema->isSingleton() && !$parameter instanceof \TYPO3\CMS\Core\SingletonInterface) {
263 $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.');
264 }
265 } elseif ($argumentInformation['hasDefaultValue'] === true) {
266 $parameter = $argumentInformation['defaultValue'];
267 } else {
268 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!', 1476107941);
269 }
270 }
271 $parameters[] = $parameter;
272 }
273 return $parameters;
274 }
275
276 /**
277 * Returns the class name for a new instance, taking into account the
278 * class-extension API.
279 *
280 * @param string $className Base class name to evaluate
281 * @return string Final class name to instantiate with "new [classname]
282 */
283 public function getImplementationClassName($className)
284 {
285 if (isset($this->alternativeImplementation[$className])) {
286 $className = $this->alternativeImplementation[$className];
287 }
288 if (substr($className, -9) === 'Interface') {
289 $className = substr($className, 0, -9);
290 }
291 return $className;
292 }
293
294 /**
295 * @param string $className
296 *
297 * @return bool
298 */
299 public function isSingleton($className)
300 {
301 return $this->getReflectionService()->getClassSchema($className)->isSingleton();
302 }
303
304 /**
305 * @param string $className
306 *
307 * @return bool
308 */
309 public function isPrototype($className)
310 {
311 return !$this->isSingleton($className);
312 }
313
314 /**
315 * @return LoggerInterface
316 */
317 protected function getLogger()
318 {
319 return GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
320 }
321
322 /**
323 * Lazy load ReflectionService.
324 *
325 * Required as this class is being loaded in ext_localconf.php and we MUST not
326 * create caches in ext_localconf.php (which ReflectionService needs to do).
327 *
328 * @return ReflectionService
329 */
330 protected function getReflectionService(): ReflectionService
331 {
332 return $this->reflectionService ?? ($this->reflectionService = GeneralUtility::makeInstance(ReflectionService::class, GeneralUtility::makeInstance(CacheManager::class)));
333 }
334 }