[+FEATURE] make DI work without inject-methods
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Object / Container / Container.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2010 Extbase Team
6 * All rights reserved
7 *
8 * This class is a backport of the corresponding class of FLOW3.
9 * All credits go to the v5 team.
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 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27
28 /**
29 * Internal TYPO3 Dependency Injection container
30 *
31 * @author Daniel Pötzinger
32 * @author Sebastian Kurfürst
33 * @author Timo Schmidt
34 */
35 class Tx_Extbase_Object_Container_Container implements t3lib_Singleton {
36
37 /**
38 * internal cache for classinfos
39 *
40 * @var Tx_Extbase_Object_Container_ClassInfoCache
41 */
42 private $cache = NULL;
43
44 /**
45 * registered alternative implementations of a class
46 * e.g. used to know the class for a AbstractClass or a Dependency
47 *
48 * @var array
49 */
50 private $alternativeImplementation;
51
52 /**
53 * reference to the classinfofactory, that analyses dependencys
54 *
55 * @var Tx_Extbase_Object_Container_ClassInfoFactory
56 */
57 private $classInfoFactory = NULL;
58
59 /**
60 * holds references of singletons
61 *
62 * @var array
63 */
64 private $singletonInstances = array();
65
66 /**
67 * Array of prototype objects currently being built, to prevent recursion.
68 *
69 * @var array
70 */
71 private $prototypeObjectsWhichAreCurrentlyInstanciated;
72
73 /**
74 * Constructor is protected since container should
75 * be a singleton.
76 *
77 * @see getContainer()
78 * @param void
79 * @return void
80 */
81 public function __construct() { }
82
83 /**
84 * Internal method to create the classInfoFactory, extracted to be mockable.
85 *
86 * @return Tx_Extbase_Object_Container_ClassInfoFactory
87 */
88 protected function getClassInfoFactory() {
89 if($this->classInfoFactory == NULL) {
90 $this->classInfoFactory = t3lib_div::makeInstance('Tx_Extbase_Object_Container_ClassInfoFactory');
91 }
92
93 return $this->classInfoFactory;
94 }
95
96 /**
97 * Internal method to create the classInfoCache, extracted to be mockable.
98 *
99 * @return Tx_Extbase_Object_Container_ClassInfoCache
100 */
101 protected function getClassInfoCache() {
102 if($this->cache == NULL) {
103 $this->cache = t3lib_div::makeInstance('Tx_Extbase_Object_Container_ClassInfoCache');
104 }
105
106 return $this->cache;
107 }
108
109 /**
110 * Main method which should be used to get an instance of the wished class
111 * specified by $className.
112 *
113 * @param string $className
114 * @param array $givenConstructorArguments the list of constructor arguments as array
115 * @return object the built object
116 */
117 public function getInstance($className, $givenConstructorArguments = array()) {
118 $this->prototypeObjectsWhichAreCurrentlyInstanciated = array();
119
120 return $this->getInstanceInternal($className, $givenConstructorArguments);
121 }
122
123 /**
124 * Create an instance of $className without calling its constructor
125 *
126 * @param string $className
127 * @return object
128 */
129 public function getEmptyObject($className) {
130 $className = $this->getImplementationClassName($className);
131 $classInfo = $this->getClassInfo($className);
132 // get an object and avoid calling __construct()
133 $object = unserialize('O:' . strlen($className) . ':"' . $className . '":0:{};');
134 $this->injectDependencies($object, $classInfo);
135 return $object;
136 }
137
138 /**
139 * Internal implementation for getting a class.
140 *
141 * @param string $className
142 * @param array $givenConstructorArguments the list of constructor arguments as array
143 * @return object the built object
144 */
145 protected function getInstanceInternal($className, $givenConstructorArguments = array()) {
146 $className = $this->getImplementationClassName($className);
147
148 if ($className === 'Tx_Extbase_Object_Container_Container') {
149 return $this;
150 }
151
152 if (isset($this->singletonInstances[$className])) {
153 if (count($givenConstructorArguments) > 0) {
154 throw new Tx_Extbase_Object_Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
155 }
156 return $this->singletonInstances[$className];
157 }
158
159 $classInfo = $this->getClassInfo($className);
160
161 $classIsSingleton = $classInfo->getIsSingleton();
162 if (!$classIsSingleton) {
163 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== FALSE) {
164 throw new Tx_Extbase_Object_Exception_CannotBuildObject('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
165 }
166 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = TRUE;
167 }
168
169 $instance = $this->instanciateObject($classInfo, $givenConstructorArguments);
170 $this->injectDependencies($instance, $classInfo);
171
172 if ($classInfo->getIsInitializeable() && is_callable(array($instance, 'initializeObject'))) {
173 $instance->initializeObject();
174 }
175
176 if (!$classIsSingleton) {
177 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
178 }
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 Tx_Extbase_Object_Container_ClassInfo $classInfo
189 * @param array $givenConstructorArguments
190 * @return object the new instance
191 */
192 protected function instanciateObject(Tx_Extbase_Object_Container_ClassInfo $classInfo, array $givenConstructorArguments) {
193 $className = $classInfo->getClassName();
194 $classIsSingleton = $classInfo->getIsSingleton();
195
196 if ($classIsSingleton && count($givenConstructorArguments) > 0) {
197 throw new Tx_Extbase_Object_Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
198 }
199
200 $constructorArguments = $this->getConstructorArguments($className, $classInfo, $givenConstructorArguments);
201 array_unshift($constructorArguments, $className);
202 $instance = call_user_func_array(array('t3lib_div', 'makeInstance'), $constructorArguments);
203
204 if ($classIsSingleton) {
205 $this->singletonInstances[$className] = $instance;
206 }
207 return $instance;
208 }
209
210 /**
211 * Inject setter-dependencies into $instance
212 *
213 * @param object $instance
214 * @param Tx_Extbase_Object_Container_ClassInfo $classInfo
215 * @return void
216 */
217 protected function injectDependencies($instance, Tx_Extbase_Object_Container_ClassInfo $classInfo) {
218 if (!$classInfo->hasInjectMethods() && !$classInfo->hasInjectProperties()) return;
219
220 foreach ($classInfo->getInjectMethods() as $injectMethodName => $classNameToInject) {
221
222 $instanceToInject = $this->getInstanceInternal($classNameToInject);
223 if ($classInfo->getIsSingleton() && !($instanceToInject instanceof t3lib_Singleton)) {
224 $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);
225 }
226
227 $instance->$injectMethodName($instanceToInject);
228 }
229
230 foreach ($classInfo->getInjectProperties() as $injectPropertyName => $classNameToInject) {
231
232 $instanceToInject = $this->getInstanceInternal($classNameToInject);
233 if ($classInfo->getIsSingleton() && !($instanceToInject instanceof t3lib_Singleton)) {
234 $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);
235 }
236
237 $propertyReflection = t3lib_div::makeInstance(
238 'Tx_Extbase_Reflection_PropertyReflection',
239 $instance,
240 $injectPropertyName
241 );
242 $propertyReflection->setAccessible(TRUE);
243 $propertyReflection->setValue($instance, $instanceToInject);
244 }
245 }
246
247 /**
248 * Wrapper for dev log, in order to ease testing
249 *
250 * @param string Message (in english).
251 * @param integer Severity: 0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
252 * @return void
253 */
254 protected function log($message, $severity) {
255 t3lib_div::devLog($message, 'extbase', $severity);
256 }
257
258 /**
259 * register a classname that should be used if a dependency is required.
260 * e.g. used to define default class for a interface
261 *
262 * @param string $className
263 * @param string $alternativeClassName
264 */
265 public function registerImplementation($className,$alternativeClassName) {
266 $this->alternativeImplementation[$className] = $alternativeClassName;
267 }
268
269 /**
270 * gets array of parameter that can be used to call a constructor
271 *
272 * @param string $className
273 * @param Tx_Extbase_Object_Container_ClassInfo $classInfo
274 * @param array $givenConstructorArguments
275 * @return array
276 */
277 private function getConstructorArguments($className, Tx_Extbase_Object_Container_ClassInfo $classInfo, array $givenConstructorArguments) {
278 $parameters=array();
279
280 $constructorArgumentInformation = $classInfo->getConstructorArguments();
281
282 foreach ($constructorArgumentInformation as $argumentInformation) {
283 $argumentName = $argumentInformation['name'];
284
285 // We have a dependency we can automatically wire,
286 // AND the class has NOT been explicitely passed in
287 if (isset($argumentInformation['dependency']) && !(count($givenConstructorArguments) && is_a($givenConstructorArguments[0], $argumentInformation['dependency']))) {
288 // Inject parameter
289 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
290 if ($classInfo->getIsSingleton() && !($parameter instanceof t3lib_Singleton)) {
291 $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);
292 }
293 } elseif (count($givenConstructorArguments)) {
294 // EITHER:
295 // No dependency injectable anymore, but we still have
296 // an explicit constructor argument
297 // OR:
298 // the passed constructor argument matches the type for the dependency
299 // injection, and thus the passed constructor takes precendence over
300 // autowiring.
301 $parameter = array_shift($givenConstructorArguments);
302 } elseif (array_key_exists('defaultValue', $argumentInformation)) {
303 // no value to set anymore, we take default value
304 $parameter = $argumentInformation['defaultValue'];
305 } else {
306 throw new InvalidArgumentException('not a correct info array of constructor dependencies was passed!');
307 }
308 $parameters[] = $parameter;
309 }
310 return $parameters;
311 }
312
313 /**
314 * Returns the class name for a new instance, taking into account the
315 * class-extension API.
316 *
317 * @param string Base class name to evaluate
318 * @return string Final class name to instantiate with "new [classname]"
319 */
320 protected function getImplementationClassName($className) {
321 if (isset($this->alternativeImplementation[$className])) {
322 $className = $this->alternativeImplementation[$className];
323 }
324
325 if (substr($className, -9) === 'Interface') {
326 $className = substr($className, 0, -9);
327 }
328
329 return $className;
330 }
331
332 /**
333 * Gets Classinfos for the className - using the cache and the factory
334 *
335 * @param string $className
336 * @return Tx_Extbase_Object_Container_ClassInfo
337 */
338 private function getClassInfo($className) {
339 // we also need to make sure that the cache is returning a vaild object
340 // in case something went wrong with unserialization etc..
341 if (!$this->getClassInfoCache()->has($className) || !is_object($this->getClassInfoCache()->get($className))) {
342 $this->getClassInfoCache()->set($className, $this->getClassInfoFactory()->buildClassInfoFromClassName($className));
343 }
344
345 return $this->getClassInfoCache()->get($className);
346 }
347 }