[TASK] Use name-resolution instead of strings where possible: 3
[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 * holds references of singletons
53 *
54 * @var array
55 */
56 private $singletonInstances = array();
57
58 /**
59 * Array of prototype objects currently being built, to prevent recursion.
60 *
61 * @var array
62 */
63 private $prototypeObjectsWhichAreCurrentlyInstanciated;
64
65 /**
66 * Constructor is protected since container should
67 * be a singleton.
68 *
69 * @see getContainer()
70 */
71 public function __construct() {
72 }
73
74 /**
75 * Internal method to create the classInfoFactory, extracted to be mockable.
76 *
77 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory
78 */
79 protected function getClassInfoFactory() {
80 if ($this->classInfoFactory == NULL) {
81 $this->classInfoFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory::class);
82 }
83 return $this->classInfoFactory;
84 }
85
86 /**
87 * Internal method to create the classInfoCache, extracted to be mockable.
88 *
89 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoCache
90 */
91 protected function getClassInfoCache() {
92 if ($this->cache == NULL) {
93 $this->cache = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\ClassInfoCache::class);
94 }
95 return $this->cache;
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 */
106 public function getInstance($className, $givenConstructorArguments = array()) {
107 $this->prototypeObjectsWhichAreCurrentlyInstanciated = array();
108 return $this->getInstanceInternal($className, $givenConstructorArguments);
109 }
110
111 /**
112 * Create an instance of $className without calling its constructor
113 *
114 * @param string $className
115 * @return object
116 */
117 public function getEmptyObject($className) {
118 $className = $this->getImplementationClassName($className);
119 $classInfo = $this->getClassInfo($className);
120 // get an object and avoid calling __construct()
121 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
122 $this->injectDependencies($object, $classInfo);
123 return $object;
124 }
125
126 /**
127 * Internal implementation for getting a class.
128 *
129 * @param string $className
130 * @param array $givenConstructorArguments the list of constructor arguments as array
131 * @throws \TYPO3\CMS\Extbase\Object\Exception
132 * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
133 * @return object the built object
134 */
135 protected function getInstanceInternal($className, $givenConstructorArguments = array()) {
136 $className = $this->getImplementationClassName($className);
137 if ($className === \TYPO3\CMS\Extbase\Object\Container\Container::class) {
138 return $this;
139 }
140 if ($className === \TYPO3\CMS\Core\Cache\CacheManager::class) {
141 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
142 }
143 if ($className === \TYPO3\CMS\Core\Package\PackageManager::class) {
144 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
145 }
146 $className = \TYPO3\CMS\Core\Core\ClassLoader::getClassNameForAlias($className);
147 if (isset($this->singletonInstances[$className])) {
148 if (count($givenConstructorArguments) > 0) {
149 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
150 }
151 return $this->singletonInstances[$className];
152 }
153 $classInfo = $this->getClassInfo($className);
154 $classIsSingleton = $classInfo->getIsSingleton();
155 if (!$classIsSingleton) {
156 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== FALSE) {
157 throw new \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
158 }
159 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = TRUE;
160 }
161 $instance = $this->instanciateObject($classInfo, $givenConstructorArguments);
162 $this->injectDependencies($instance, $classInfo);
163 if ($classInfo->getIsInitializeable() && is_callable(array($instance, 'initializeObject'))) {
164 $instance->initializeObject();
165 }
166 if (!$classIsSingleton) {
167 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
168 }
169 return $instance;
170 }
171
172 /**
173 * Instanciates an object, possibly setting the constructor dependencies.
174 * Additionally, directly registers all singletons in the singleton registry,
175 * such that circular references of singletons are correctly instanciated.
176 *
177 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
178 * @param array $givenConstructorArguments
179 * @throws \TYPO3\CMS\Extbase\Object\Exception
180 * @return object the new instance
181 */
182 protected function instanciateObject(\TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
183 $className = $classInfo->getClassName();
184 $classIsSingleton = $classInfo->getIsSingleton();
185 if ($classIsSingleton && count($givenConstructorArguments) > 0) {
186 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
187 }
188 $constructorArguments = $this->getConstructorArguments($className, $classInfo, $givenConstructorArguments);
189 array_unshift($constructorArguments, $className);
190 $instance = call_user_func_array(array(\TYPO3\CMS\Core\Utility\GeneralUtility::class, 'makeInstance'), $constructorArguments);
191 if ($classIsSingleton) {
192 $this->singletonInstances[$className] = $instance;
193 }
194 return $instance;
195 }
196
197 /**
198 * Inject setter-dependencies into $instance
199 *
200 * @param object $instance
201 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
202 * @return void
203 */
204 protected function injectDependencies($instance, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo) {
205 if (!$classInfo->hasInjectMethods() && !$classInfo->hasInjectProperties()) {
206 return;
207 }
208 foreach ($classInfo->getInjectMethods() as $injectMethodName => $classNameToInject) {
209 $instanceToInject = $this->getInstanceInternal($classNameToInject);
210 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
211 $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);
212 }
213 if (is_callable(array($instance, $injectMethodName))) {
214 $instance->{$injectMethodName}($instanceToInject);
215 }
216 }
217 foreach ($classInfo->getInjectProperties() as $injectPropertyName => $classNameToInject) {
218 $instanceToInject = $this->getInstanceInternal($classNameToInject);
219 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
220 $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);
221 }
222 $propertyReflection = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Reflection\PropertyReflection::class, $instance, $injectPropertyName);
223
224 $propertyReflection->setAccessible(TRUE);
225 $propertyReflection->setValue($instance, $instanceToInject);
226 }
227 }
228
229 /**
230 * Wrapper for dev log, in order to ease testing
231 *
232 * @param string $message Message (in english).
233 * @param int $severity Severity: 0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
234 * @return void
235 */
236 protected function log($message, $severity) {
237 \TYPO3\CMS\Core\Utility\GeneralUtility::devLog($message, 'extbase', $severity);
238 }
239
240 /**
241 * register a classname that should be used if a dependency is required.
242 * e.g. used to define default class for a interface
243 *
244 * @param string $className
245 * @param string $alternativeClassName
246 */
247 public function registerImplementation($className, $alternativeClassName) {
248 $this->alternativeImplementation[$className] = $alternativeClassName;
249 }
250
251 /**
252 * gets array of parameter that can be used to call a constructor
253 *
254 * @param string $className
255 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
256 * @param array $givenConstructorArguments
257 * @throws \InvalidArgumentException
258 * @return array
259 */
260 private function getConstructorArguments($className, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
261 $parameters = array();
262 $constructorArgumentInformation = $classInfo->getConstructorArguments();
263 foreach ($constructorArgumentInformation as $index => $argumentInformation) {
264 // Constructor argument given AND argument is a simple type OR instance of argument type
265 if (array_key_exists($index, $givenConstructorArguments) && (!isset($argumentInformation['dependency']) || is_a($givenConstructorArguments[$index], $argumentInformation['dependency']))) {
266 $parameter = $givenConstructorArguments[$index];
267 } else {
268 if (isset($argumentInformation['dependency']) && !array_key_exists('defaultValue', $argumentInformation)) {
269 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
270 if ($classInfo->getIsSingleton() && !$parameter instanceof \TYPO3\CMS\Core\SingletonInterface) {
271 $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);
272 }
273 } elseif (array_key_exists('defaultValue', $argumentInformation)) {
274 $parameter = $argumentInformation['defaultValue'];
275 } else {
276 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!');
277 }
278 }
279 $parameters[] = $parameter;
280 }
281 return $parameters;
282 }
283
284 /**
285 * Returns the class name for a new instance, taking into account the
286 * class-extension API.
287 *
288 * @param string $className Base class name to evaluate
289 * @return string Final class name to instantiate with "new [classname]
290 */
291 public function getImplementationClassName($className) {
292 if (isset($this->alternativeImplementation[$className])) {
293 $className = $this->alternativeImplementation[$className];
294 }
295 if (substr($className, -9) === 'Interface') {
296 $className = substr($className, 0, -9);
297 }
298 return $className;
299 }
300
301 /**
302 * Gets Classinfos for the className - using the cache and the factory
303 *
304 * @param string $className
305 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfo
306 */
307 private function getClassInfo($className) {
308 $classNameHash = md5($className);
309 $classInfo = $this->getClassInfoCache()->get($classNameHash);
310 if (!$classInfo instanceof \TYPO3\CMS\Extbase\Object\Container\ClassInfo) {
311 $classInfo = $this->getClassInfoFactory()->buildClassInfoFromClassName($className);
312 $this->getClassInfoCache()->set($classNameHash, $classInfo);
313 }
314 return $classInfo;
315 }
316
317 /**
318 * @param string $className
319 *
320 * @return bool
321 */
322 public function isSingleton($className) {
323 return $this->getClassInfo($className)->getIsSingleton();
324 }
325
326 /**
327 * @param string $className
328 *
329 * @return bool
330 */
331 public function isPrototype($className) {
332 return !$this->isSingleton($className);
333 }
334 }