[!!!][FEATURE] Discard TYPO3 class loader
[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 /**
18 * Internal TYPO3 Dependency Injection container
19 *
20 * @author Daniel Pötzinger
21 * @author Sebastian Kurfürst
22 * @author Timo Schmidt
23 */
24 class Container implements \TYPO3\CMS\Core\SingletonInterface {
25
26 const SCOPE_PROTOTYPE = 1;
27 const SCOPE_SINGLETON = 2;
28
29 /**
30 * internal cache for classinfos
31 *
32 * @var \TYPO3\CMS\Extbase\Object\Container\ClassInfoCache
33 */
34 private $cache = NULL;
35
36 /**
37 * registered alternative implementations of a class
38 * e.g. used to know the class for a AbstractClass or a Dependency
39 *
40 * @var array
41 */
42 private $alternativeImplementation;
43
44 /**
45 * reference to the classinfofactory, that analyses dependencys
46 *
47 * @var \TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory
48 */
49 private $classInfoFactory = NULL;
50
51 /**
52 * @var \Doctrine\Instantiator\InstantiatorInterface
53 */
54 protected $instantiator = NULL;
55
56 /**
57 * holds references of singletons
58 *
59 * @var array
60 */
61 private $singletonInstances = array();
62
63 /**
64 * Array of prototype objects currently being built, to prevent recursion.
65 *
66 * @var array
67 */
68 private $prototypeObjectsWhichAreCurrentlyInstanciated;
69
70 /**
71 * Constructor is protected since container should
72 * be a singleton.
73 *
74 * @see getContainer()
75 */
76 public function __construct() {
77 }
78
79 /**
80 * Internal method to create the classInfoFactory, extracted to be mockable.
81 *
82 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory
83 */
84 protected function getClassInfoFactory() {
85 if ($this->classInfoFactory == NULL) {
86 $this->classInfoFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory::class);
87 }
88 return $this->classInfoFactory;
89 }
90
91 /**
92 * Internal method to create the classInfoCache, extracted to be mockable.
93 *
94 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoCache
95 */
96 protected function getClassInfoCache() {
97 if ($this->cache == NULL) {
98 $this->cache = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\ClassInfoCache::class);
99 }
100 return $this->cache;
101 }
102
103 /**
104 * Internal method to create the class instantiator, extracted to be mockable
105 *
106 * @return \Doctrine\Instantiator\InstantiatorInterface
107 */
108 protected function getInstantiator() {
109 if ($this->instantiator == NULL) {
110 $this->instantiator = new \Doctrine\Instantiator\Instantiator();
111 }
112 return $this->instantiator;
113 }
114
115 /**
116 * Main method which should be used to get an instance of the wished class
117 * specified by $className.
118 *
119 * @param string $className
120 * @param array $givenConstructorArguments the list of constructor arguments as array
121 * @return object the built object
122 */
123 public function getInstance($className, $givenConstructorArguments = array()) {
124 $this->prototypeObjectsWhichAreCurrentlyInstanciated = array();
125 return $this->getInstanceInternal($className, $givenConstructorArguments);
126 }
127
128 /**
129 * Create an instance of $className without calling its constructor
130 *
131 * @param string $className
132 * @return object
133 */
134 public function getEmptyObject($className) {
135 $className = $this->getImplementationClassName($className);
136 $classInfo = $this->getClassInfo($className);
137 $object = $this->getInstantiator()->instantiate($className);
138 $this->injectDependencies($object, $classInfo);
139 $this->initializeObject($object, $classInfo);
140 return $object;
141 }
142
143 /**
144 * Internal implementation for getting a class.
145 *
146 * @param string $className
147 * @param array $givenConstructorArguments the list of constructor arguments as array
148 * @throws \TYPO3\CMS\Extbase\Object\Exception
149 * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
150 * @return object the built object
151 */
152 protected function getInstanceInternal($className, $givenConstructorArguments = array()) {
153 $className = $this->getImplementationClassName($className);
154 if ($className === \TYPO3\CMS\Extbase\Object\Container\Container::class) {
155 return $this;
156 }
157 if ($className === \TYPO3\CMS\Core\Cache\CacheManager::class) {
158 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
159 }
160 if ($className === \TYPO3\CMS\Core\Package\PackageManager::class) {
161 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
162 }
163 $className = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($className);
164 if (isset($this->singletonInstances[$className])) {
165 if (count($givenConstructorArguments) > 0) {
166 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
167 }
168 return $this->singletonInstances[$className];
169 }
170 $classInfo = $this->getClassInfo($className);
171 $classIsSingleton = $classInfo->getIsSingleton();
172 if (!$classIsSingleton) {
173 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== FALSE) {
174 throw new \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
175 }
176 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = TRUE;
177 }
178 $instance = $this->instanciateObject($classInfo, $givenConstructorArguments);
179 $this->injectDependencies($instance, $classInfo);
180 $this->initializeObject($instance, $classInfo);
181 if (!$classIsSingleton) {
182 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
183 }
184 return $instance;
185 }
186
187 /**
188 * Instanciates an object, possibly setting the constructor dependencies.
189 * Additionally, directly registers all singletons in the singleton registry,
190 * such that circular references of singletons are correctly instanciated.
191 *
192 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
193 * @param array $givenConstructorArguments
194 * @throws \TYPO3\CMS\Extbase\Object\Exception
195 * @return object the new instance
196 */
197 protected function instanciateObject(\TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
198 $className = $classInfo->getClassName();
199 $classIsSingleton = $classInfo->getIsSingleton();
200 if ($classIsSingleton && count($givenConstructorArguments) > 0) {
201 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
202 }
203 $constructorArguments = $this->getConstructorArguments($className, $classInfo, $givenConstructorArguments);
204 array_unshift($constructorArguments, $className);
205 $instance = call_user_func_array(array(\TYPO3\CMS\Core\Utility\GeneralUtility::class, 'makeInstance'), $constructorArguments);
206 if ($classIsSingleton) {
207 $this->singletonInstances[$className] = $instance;
208 }
209 return $instance;
210 }
211
212 /**
213 * Inject setter-dependencies into $instance
214 *
215 * @param object $instance
216 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
217 * @return void
218 */
219 protected function injectDependencies($instance, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo) {
220 if (!$classInfo->hasInjectMethods() && !$classInfo->hasInjectProperties()) {
221 return;
222 }
223 foreach ($classInfo->getInjectMethods() as $injectMethodName => $classNameToInject) {
224 $instanceToInject = $this->getInstanceInternal($classNameToInject);
225 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
226 $this->log('The singleton "' . $classInfo->getClassName() . '" needs a prototype in "' . $injectMethodName . '". This is often a bad code smell; often you rather want to inject a singleton.', 1);
227 }
228 if (is_callable(array($instance, $injectMethodName))) {
229 $instance->{$injectMethodName}($instanceToInject);
230 }
231 }
232 foreach ($classInfo->getInjectProperties() as $injectPropertyName => $classNameToInject) {
233 $instanceToInject = $this->getInstanceInternal($classNameToInject);
234 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
235 $this->log('The singleton "' . $classInfo->getClassName() . '" needs a prototype in "' . $injectPropertyName . '". This is often a bad code smell; often you rather want to inject a singleton.', 1);
236 }
237 $propertyReflection = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Reflection\PropertyReflection::class, $instance, $injectPropertyName);
238
239 $propertyReflection->setAccessible(TRUE);
240 $propertyReflection->setValue($instance, $instanceToInject);
241 }
242 }
243
244 /**
245 * Initialize $instance after injecting all dependencies
246 *
247 * @param object $instance
248 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
249 */
250 protected function initializeObject($instance, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo) {
251 if ($classInfo->getIsInitializeable() && is_callable(array($instance, 'initializeObject'))) {
252 $instance->initializeObject();
253 }
254 }
255
256 /**
257 * Wrapper for dev log, in order to ease testing
258 *
259 * @param string $message Message (in english).
260 * @param int $severity Severity: 0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
261 * @return void
262 */
263 protected function log($message, $severity) {
264 \TYPO3\CMS\Core\Utility\GeneralUtility::devLog($message, 'extbase', $severity);
265 }
266
267 /**
268 * register a classname that should be used if a dependency is required.
269 * e.g. used to define default class for a interface
270 *
271 * @param string $className
272 * @param string $alternativeClassName
273 */
274 public function registerImplementation($className, $alternativeClassName) {
275 $this->alternativeImplementation[$className] = $alternativeClassName;
276 }
277
278 /**
279 * gets array of parameter that can be used to call a constructor
280 *
281 * @param string $className
282 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
283 * @param array $givenConstructorArguments
284 * @throws \InvalidArgumentException
285 * @return array
286 */
287 private function getConstructorArguments($className, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
288 $parameters = array();
289 $constructorArgumentInformation = $classInfo->getConstructorArguments();
290 foreach ($constructorArgumentInformation as $index => $argumentInformation) {
291 // Constructor argument given AND argument is a simple type OR instance of argument type
292 if (array_key_exists($index, $givenConstructorArguments) && (!isset($argumentInformation['dependency']) || is_a($givenConstructorArguments[$index], $argumentInformation['dependency']))) {
293 $parameter = $givenConstructorArguments[$index];
294 } else {
295 if (isset($argumentInformation['dependency']) && !array_key_exists('defaultValue', $argumentInformation)) {
296 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
297 if ($classInfo->getIsSingleton() && !$parameter instanceof \TYPO3\CMS\Core\SingletonInterface) {
298 $this->log('The singleton "' . $className . '" needs a prototype in the constructor. This is often a bad code smell; often you rather want to inject a singleton.', 1);
299 }
300 } elseif (array_key_exists('defaultValue', $argumentInformation)) {
301 $parameter = $argumentInformation['defaultValue'];
302 } else {
303 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!');
304 }
305 }
306 $parameters[] = $parameter;
307 }
308 return $parameters;
309 }
310
311 /**
312 * Returns the class name for a new instance, taking into account the
313 * class-extension API.
314 *
315 * @param string $className Base class name to evaluate
316 * @return string Final class name to instantiate with "new [classname]
317 */
318 public function getImplementationClassName($className) {
319 if (isset($this->alternativeImplementation[$className])) {
320 $className = $this->alternativeImplementation[$className];
321 }
322 if (substr($className, -9) === 'Interface') {
323 $className = substr($className, 0, -9);
324 }
325 return $className;
326 }
327
328 /**
329 * Gets Classinfos for the className - using the cache and the factory
330 *
331 * @param string $className
332 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfo
333 */
334 private function getClassInfo($className) {
335 $classNameHash = md5($className);
336 $classInfo = $this->getClassInfoCache()->get($classNameHash);
337 if (!$classInfo instanceof \TYPO3\CMS\Extbase\Object\Container\ClassInfo) {
338 $classInfo = $this->getClassInfoFactory()->buildClassInfoFromClassName($className);
339 $this->getClassInfoCache()->set($classNameHash, $classInfo);
340 }
341 return $classInfo;
342 }
343
344 /**
345 * @param string $className
346 *
347 * @return bool
348 */
349 public function isSingleton($className) {
350 return $this->getClassInfo($className)->getIsSingleton();
351 }
352
353 /**
354 * @param string $className
355 *
356 * @return bool
357 */
358 public function isPrototype($className) {
359 return !$this->isSingleton($className);
360 }
361 }