[BUGFIX] Omit constructor injection for optional parameters
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Object / Container / Container.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Object\Container;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Extbase Team (http://forge.typo3.org/projects/typo3v4-mvc)
8 * Extbase is a backport of TYPO3 Flow. All credits go to the TYPO3 Flow team.
9 * All rights reserved
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 * A copy is found in the textfile GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30
31 /**
32 * Internal TYPO3 Dependency Injection container
33 *
34 * @author Daniel Pötzinger
35 * @author Sebastian Kurfürst
36 * @author Timo Schmidt
37 */
38 class Container implements \TYPO3\CMS\Core\SingletonInterface {
39
40 const SCOPE_PROTOTYPE = 1;
41 const SCOPE_SINGLETON = 2;
42
43 /**
44 * internal cache for classinfos
45 *
46 * @var \TYPO3\CMS\Extbase\Object\Container\ClassInfoCache
47 */
48 private $cache = NULL;
49
50 /**
51 * registered alternative implementations of a class
52 * e.g. used to know the class for a AbstractClass or a Dependency
53 *
54 * @var array
55 */
56 private $alternativeImplementation;
57
58 /**
59 * reference to the classinfofactory, that analyses dependencys
60 *
61 * @var \TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory
62 */
63 private $classInfoFactory = NULL;
64
65 /**
66 * holds references of singletons
67 *
68 * @var array
69 */
70 private $singletonInstances = array();
71
72 /**
73 * Array of prototype objects currently being built, to prevent recursion.
74 *
75 * @var array
76 */
77 private $prototypeObjectsWhichAreCurrentlyInstanciated;
78
79 /**
80 * Constructor is protected since container should
81 * be a singleton.
82 *
83 * @see getContainer()
84 */
85 public function __construct() {
86 }
87
88 /**
89 * Internal method to create the classInfoFactory, extracted to be mockable.
90 *
91 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoFactory
92 */
93 protected function getClassInfoFactory() {
94 if ($this->classInfoFactory == NULL) {
95 $this->classInfoFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\Container\\ClassInfoFactory');
96 }
97 return $this->classInfoFactory;
98 }
99
100 /**
101 * Internal method to create the classInfoCache, extracted to be mockable.
102 *
103 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfoCache
104 */
105 protected function getClassInfoCache() {
106 if ($this->cache == NULL) {
107 $this->cache = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\Container\\ClassInfoCache');
108 }
109 return $this->cache;
110 }
111
112 /**
113 * Main method which should be used to get an instance of the wished class
114 * specified by $className.
115 *
116 * @param string $className
117 * @param array $givenConstructorArguments the list of constructor arguments as array
118 * @return object the built object
119 */
120 public function getInstance($className, $givenConstructorArguments = array()) {
121 $this->prototypeObjectsWhichAreCurrentlyInstanciated = array();
122 return $this->getInstanceInternal($className, $givenConstructorArguments);
123 }
124
125 /**
126 * Create an instance of $className without calling its constructor
127 *
128 * @param string $className
129 * @return object
130 */
131 public function getEmptyObject($className) {
132 $className = $this->getImplementationClassName($className);
133 $classInfo = $this->getClassInfo($className);
134 // get an object and avoid calling __construct()
135 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
136 $this->injectDependencies($object, $classInfo);
137 return $object;
138 }
139
140 /**
141 * Internal implementation for getting a class.
142 *
143 * @param string $className
144 * @param array $givenConstructorArguments the list of constructor arguments as array
145 * @throws \TYPO3\CMS\Extbase\Object\Exception
146 * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
147 * @return object the built object
148 */
149 protected function getInstanceInternal($className, $givenConstructorArguments = array()) {
150 $className = $this->getImplementationClassName($className);
151 if ($className === 'TYPO3\\CMS\\Extbase\\Object\\Container\\Container') {
152 return $this;
153 }
154 if ($className === 'TYPO3\\CMS\\Core\\Cache\\CacheManager') {
155 return $GLOBALS['typo3CacheManager'];
156 }
157 $className = \TYPO3\CMS\Core\Core\ClassLoader::getClassNameForAlias($className);
158 if (isset($this->singletonInstances[$className])) {
159 if (count($givenConstructorArguments) > 0) {
160 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
161 }
162 return $this->singletonInstances[$className];
163 }
164 $classInfo = $this->getClassInfo($className);
165 $classIsSingleton = $classInfo->getIsSingleton();
166 if (!$classIsSingleton) {
167 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== FALSE) {
168 throw new \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
169 }
170 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = TRUE;
171 }
172 $instance = $this->instanciateObject($classInfo, $givenConstructorArguments);
173 $this->injectDependencies($instance, $classInfo);
174 if ($classInfo->getIsInitializeable() && is_callable(array($instance, 'initializeObject'))) {
175 $instance->initializeObject();
176 }
177 if (!$classIsSingleton) {
178 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
179 }
180 return $instance;
181 }
182
183 /**
184 * Instanciates an object, possibly setting the constructor dependencies.
185 * Additionally, directly registers all singletons in the singleton registry,
186 * such that circular references of singletons are correctly instanciated.
187 *
188 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
189 * @param array $givenConstructorArguments
190 * @throws \TYPO3\CMS\Extbase\Object\Exception
191 * @return object the new instance
192 */
193 protected function instanciateObject(\TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
194 $className = $classInfo->getClassName();
195 $classIsSingleton = $classInfo->getIsSingleton();
196 if ($classIsSingleton && count($givenConstructorArguments) > 0) {
197 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
198 }
199 $constructorArguments = $this->getConstructorArguments($className, $classInfo, $givenConstructorArguments);
200 array_unshift($constructorArguments, $className);
201 $instance = call_user_func_array(array('TYPO3\\CMS\\Core\\Utility\\GeneralUtility', 'makeInstance'), $constructorArguments);
202 if ($classIsSingleton) {
203 $this->singletonInstances[$className] = $instance;
204 }
205 return $instance;
206 }
207
208 /**
209 * Inject setter-dependencies into $instance
210 *
211 * @param object $instance
212 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
213 * @return void
214 */
215 protected function injectDependencies($instance, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo) {
216 if (!$classInfo->hasInjectMethods() && !$classInfo->hasInjectProperties()) {
217 return;
218 }
219 foreach ($classInfo->getInjectMethods() as $injectMethodName => $classNameToInject) {
220 $instanceToInject = $this->getInstanceInternal($classNameToInject);
221 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
222 $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);
223 }
224 if (is_callable(array($instance, $injectMethodName))) {
225 $instance->{$injectMethodName}($instanceToInject);
226 }
227 }
228 foreach ($classInfo->getInjectProperties() as $injectPropertyName => $classNameToInject) {
229 $instanceToInject = $this->getInstanceInternal($classNameToInject);
230 if ($classInfo->getIsSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
231 $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.', 1320177676);
232 }
233 $propertyReflection = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\PropertyReflection', $instance, $injectPropertyName);
234
235 $propertyReflection->setAccessible(TRUE);
236 $propertyReflection->setValue($instance, $instanceToInject);
237 }
238 }
239
240 /**
241 * Wrapper for dev log, in order to ease testing
242 *
243 * @param string $message Message (in english).
244 * @param integer $severity Severity: 0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
245 * @return void
246 */
247 protected function log($message, $severity) {
248 \TYPO3\CMS\Core\Utility\GeneralUtility::devLog($message, 'extbase', $severity);
249 }
250
251 /**
252 * register a classname that should be used if a dependency is required.
253 * e.g. used to define default class for a interface
254 *
255 * @param string $className
256 * @param string $alternativeClassName
257 */
258 public function registerImplementation($className, $alternativeClassName) {
259 $this->alternativeImplementation[$className] = $alternativeClassName;
260 }
261
262 /**
263 * gets array of parameter that can be used to call a constructor
264 *
265 * @param string $className
266 * @param \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo
267 * @param array $givenConstructorArguments
268 * @throws \InvalidArgumentException
269 * @return array
270 */
271 private function getConstructorArguments($className, \TYPO3\CMS\Extbase\Object\Container\ClassInfo $classInfo, array $givenConstructorArguments) {
272 $parameters = array();
273 $constructorArgumentInformation = $classInfo->getConstructorArguments();
274 foreach ($constructorArgumentInformation as $index => $argumentInformation) {
275 // Constructor argument given AND argument is a simple type OR instance of argument type
276 if (array_key_exists($index, $givenConstructorArguments) && (!isset($argumentInformation['dependency']) || is_a($givenConstructorArguments[$index], $argumentInformation['dependency']))) {
277 $parameter = $givenConstructorArguments[$index];
278 } else {
279 if (isset($argumentInformation['dependency']) && !array_key_exists('defaultValue', $argumentInformation)) {
280 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
281 if ($classInfo->getIsSingleton() && !$parameter instanceof \TYPO3\CMS\Core\SingletonInterface) {
282 $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);
283 }
284 } elseif (array_key_exists('defaultValue', $argumentInformation)) {
285 $parameter = $argumentInformation['defaultValue'];
286 } else {
287 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!');
288 }
289 }
290 $parameters[] = $parameter;
291 }
292 return $parameters;
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 protected function getImplementationClassName($className) {
303 if (isset($this->alternativeImplementation[$className])) {
304 $className = $this->alternativeImplementation[$className];
305 }
306 if (substr($className, -9) === 'Interface') {
307 $className = substr($className, 0, -9);
308 }
309 return $className;
310 }
311
312 /**
313 * Gets Classinfos for the className - using the cache and the factory
314 *
315 * @param string $className
316 * @return \TYPO3\CMS\Extbase\Object\Container\ClassInfo
317 */
318 private function getClassInfo($className) {
319 $classNameHash = md5($className);
320 $classInfo = $this->getClassInfoCache()->get($classNameHash);
321 if (!$classInfo instanceof \TYPO3\CMS\Extbase\Object\Container\ClassInfo) {
322 $classInfo = $this->getClassInfoFactory()->buildClassInfoFromClassName($className);
323 $this->getClassInfoCache()->set($classNameHash, $classInfo);
324 }
325 return $classInfo;
326 }
327
328 /**
329 * @param string $className
330 *
331 * @return boolean
332 */
333 public function isSingleton($className) {
334 return $this->getClassInfo($className)->getIsSingleton();
335 }
336
337 /**
338 * @param string $className
339 *
340 * @return boolean
341 */
342 public function isPrototype($className) {
343 return !$this->isSingleton($className);
344 }
345 }