[BUGFIX] Avoid reflection for public property injection
[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 */
27 class Container implements \TYPO3\CMS\Core\SingletonInterface
28 {
29 const SCOPE_PROTOTYPE = 1;
30 const SCOPE_SINGLETON = 2;
31
32 /**
33 * registered alternative implementations of a class
34 * e.g. used to know the class for an AbstractClass or a Dependency
35 *
36 * @var array
37 */
38 private $alternativeImplementation;
39
40 /**
41 * @var \Doctrine\Instantiator\InstantiatorInterface
42 */
43 protected $instantiator = null;
44
45 /**
46 * holds references of singletons
47 *
48 * @var array
49 */
50 private $singletonInstances = [];
51
52 /**
53 * Array of prototype objects currently being built, to prevent recursion.
54 *
55 * @var array
56 */
57 private $prototypeObjectsWhichAreCurrentlyInstanciated;
58
59 /**
60 * @var ReflectionService
61 */
62 private $reflectionService;
63
64 /**
65 * Constructor is protected since container should
66 * be a singleton.
67 *
68 * @see getContainer()
69 */
70 public function __construct()
71 {
72 $this->reflectionService = GeneralUtility::makeInstance(ReflectionService::class, GeneralUtility::makeInstance(CacheManager::class));
73 }
74
75 /**
76 * Internal method to create the class instantiator, extracted to be mockable
77 *
78 * @return \Doctrine\Instantiator\InstantiatorInterface
79 */
80 protected function getInstantiator()
81 {
82 if ($this->instantiator == null) {
83 $this->instantiator = new \Doctrine\Instantiator\Instantiator();
84 }
85 return $this->instantiator;
86 }
87
88 /**
89 * Main method which should be used to get an instance of the wished class
90 * specified by $className.
91 *
92 * @param string $className
93 * @param array $givenConstructorArguments the list of constructor arguments as array
94 * @return object the built object
95 */
96 public function getInstance($className, $givenConstructorArguments = [])
97 {
98 $this->prototypeObjectsWhichAreCurrentlyInstanciated = [];
99 return $this->getInstanceInternal($className, $givenConstructorArguments);
100 }
101
102 /**
103 * Create an instance of $className without calling its constructor
104 *
105 * @param string $className
106 * @return object
107 */
108 public function getEmptyObject($className)
109 {
110 $className = $this->getImplementationClassName($className);
111 $classSchema = $this->reflectionService->getClassSchema($className);
112 $object = $this->getInstantiator()->instantiate($className);
113 $this->injectDependencies($object, $classSchema);
114 $this->initializeObject($object);
115 return $object;
116 }
117
118 /**
119 * Internal implementation for getting a class.
120 *
121 * @param string $className
122 * @param array $givenConstructorArguments the list of constructor arguments as array
123 * @throws \TYPO3\CMS\Extbase\Object\Exception
124 * @throws \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException
125 * @return object the built object
126 */
127 protected function getInstanceInternal($className, $givenConstructorArguments = [])
128 {
129 $className = $this->getImplementationClassName($className);
130 if ($className === \TYPO3\CMS\Extbase\Object\Container\Container::class) {
131 return $this;
132 }
133 if ($className === \TYPO3\CMS\Core\Cache\CacheManager::class) {
134 return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
135 }
136 if ($className === \TYPO3\CMS\Core\Package\PackageManager::class) {
137 return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
138 }
139 $className = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($className);
140 if (isset($this->singletonInstances[$className])) {
141 if (!empty($givenConstructorArguments)) {
142 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" fetched from singleton cache, thus, explicit constructor arguments are not allowed.', 1292857934);
143 }
144 return $this->singletonInstances[$className];
145 }
146
147 $classSchema = $this->reflectionService->getClassSchema($className);
148 $classIsSingleton = $classSchema->isSingleton();
149 if (!$classIsSingleton) {
150 if (array_key_exists($className, $this->prototypeObjectsWhichAreCurrentlyInstanciated) !== false) {
151 throw new \TYPO3\CMS\Extbase\Object\Exception\CannotBuildObjectException('Cyclic dependency in prototype object, for class "' . $className . '".', 1295611406);
152 }
153 $this->prototypeObjectsWhichAreCurrentlyInstanciated[$className] = true;
154 }
155 $instance = $this->instanciateObject($classSchema, $givenConstructorArguments);
156 $this->injectDependencies($instance, $classSchema);
157 $this->initializeObject($instance);
158 if (!$classIsSingleton) {
159 unset($this->prototypeObjectsWhichAreCurrentlyInstanciated[$className]);
160 }
161 return $instance;
162 }
163
164 /**
165 * Instanciates an object, possibly setting the constructor dependencies.
166 * Additionally, directly registers all singletons in the singleton registry,
167 * such that circular references of singletons are correctly instanciated.
168 *
169 * @param ClassSchema $classSchema
170 * @param array $givenConstructorArguments
171 * @throws \TYPO3\CMS\Extbase\Object\Exception
172 * @return object the new instance
173 */
174 protected function instanciateObject(ClassSchema $classSchema, array $givenConstructorArguments)
175 {
176 $className = $classSchema->getClassName();
177 $classIsSingleton = $classSchema->isSingleton();
178 if ($classIsSingleton && !empty($givenConstructorArguments)) {
179 throw new \TYPO3\CMS\Extbase\Object\Exception('Object "' . $className . '" has explicit constructor arguments but is a singleton; this is not allowed.', 1292858051);
180 }
181 $constructorArguments = $this->getConstructorArguments($className, $classSchema, $givenConstructorArguments);
182 array_unshift($constructorArguments, $className);
183 $instance = call_user_func_array([GeneralUtility::class, 'makeInstance'], $constructorArguments);
184 if ($classIsSingleton) {
185 $this->singletonInstances[$className] = $instance;
186 }
187 return $instance;
188 }
189
190 /**
191 * Inject setter-dependencies into $instance
192 *
193 * @param object $instance
194 * @param ClassSchema $classSchema
195 */
196 protected function injectDependencies($instance, ClassSchema $classSchema)
197 {
198 if (!$classSchema->hasInjectMethods() && !$classSchema->hasInjectProperties()) {
199 return;
200 }
201 foreach ($classSchema->getInjectMethods() as $injectMethodName => $classNameToInject) {
202 $instanceToInject = $this->getInstanceInternal($classNameToInject);
203 if ($classSchema->isSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
204 $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.');
205 }
206 if (is_callable([$instance, $injectMethodName])) {
207 $instance->{$injectMethodName}($instanceToInject);
208 }
209 }
210 foreach ($classSchema->getInjectProperties() as $injectPropertyName => $classNameToInject) {
211 $instanceToInject = $this->getInstanceInternal($classNameToInject);
212 if ($classSchema->isSingleton() && !$instanceToInject instanceof \TYPO3\CMS\Core\SingletonInterface) {
213 $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.');
214 }
215
216 if ($classSchema->getProperty($injectPropertyName)['public']) {
217 $instance->{$injectPropertyName} = $instanceToInject;
218 } else {
219 $propertyReflection = new \ReflectionProperty($instance, $injectPropertyName);
220 $propertyReflection->setAccessible(true);
221 $propertyReflection->setValue($instance, $instanceToInject);
222 }
223 }
224 }
225
226 /**
227 * Call object initializer if present in object
228 *
229 * @param object $instance
230 */
231 protected function initializeObject($instance)
232 {
233 if (method_exists($instance, 'initializeObject') && is_callable([$instance, 'initializeObject'])) {
234 $instance->initializeObject();
235 }
236 }
237
238 /**
239 * register a classname that should be used if a dependency is required.
240 * e.g. used to define default class for an interface
241 *
242 * @param string $className
243 * @param string $alternativeClassName
244 */
245 public function registerImplementation($className, $alternativeClassName)
246 {
247 $this->alternativeImplementation[$className] = $alternativeClassName;
248 }
249
250 /**
251 * gets array of parameter that can be used to call a constructor
252 *
253 * @param string $className
254 * @param ClassSchema $classSchema
255 * @param array $givenConstructorArguments
256 * @throws \InvalidArgumentException
257 * @return array
258 */
259 private function getConstructorArguments($className, ClassSchema $classSchema, array $givenConstructorArguments)
260 {
261 $parameters = [];
262 $constructorArgumentInformation = $classSchema->getConstructorArguments();
263 foreach ($constructorArgumentInformation as $constructorArgumentName => $argumentInformation) {
264 $index = $argumentInformation['position'];
265
266 // Constructor argument given AND argument is a simple type OR instance of argument type
267 if (array_key_exists($index, $givenConstructorArguments) && (!isset($argumentInformation['dependency']) || is_a($givenConstructorArguments[$index], $argumentInformation['dependency']))) {
268 $parameter = $givenConstructorArguments[$index];
269 } else {
270 if (isset($argumentInformation['dependency']) && $argumentInformation['hasDefaultValue'] === false) {
271 $parameter = $this->getInstanceInternal($argumentInformation['dependency']);
272 if ($classSchema->isSingleton() && !$parameter instanceof \TYPO3\CMS\Core\SingletonInterface) {
273 $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.');
274 }
275 } elseif ($argumentInformation['hasDefaultValue'] === true) {
276 $parameter = $argumentInformation['defaultValue'];
277 } else {
278 throw new \InvalidArgumentException('not a correct info array of constructor dependencies was passed!', 1476107941);
279 }
280 }
281 $parameters[] = $parameter;
282 }
283 return $parameters;
284 }
285
286 /**
287 * Returns the class name for a new instance, taking into account the
288 * class-extension API.
289 *
290 * @param string $className Base class name to evaluate
291 * @return string Final class name to instantiate with "new [classname]
292 */
293 public function getImplementationClassName($className)
294 {
295 if (isset($this->alternativeImplementation[$className])) {
296 $className = $this->alternativeImplementation[$className];
297 }
298 if (substr($className, -9) === 'Interface') {
299 $className = substr($className, 0, -9);
300 }
301 return $className;
302 }
303
304 /**
305 * @param string $className
306 *
307 * @return bool
308 */
309 public function isSingleton($className)
310 {
311 return $this->reflectionService->getClassSchema($className)->isSingleton();
312 }
313
314 /**
315 * @param string $className
316 *
317 * @return bool
318 */
319 public function isPrototype($className)
320 {
321 return !$this->isSingleton($className);
322 }
323
324 /**
325 * @return LoggerInterface
326 */
327 protected function getLogger()
328 {
329 return GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
330 }
331 }