DefaultFactory.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\core\impl;
12 
16 use wcmf\lib\util\StringUtil;
17 
18 /**
19  * DefaultFactory is used to create service instances. The concrete service
20  * implementations and building parameters are described in the given configuration.
21  * Dependencies are injected in the following ways:
22  *
23  * - _Constructor injection_: Each constructor parameter is resolved either from
24  * the instance configuration or, if not specified, from an existing class that
25  * is of the same type as defined in the type hinting.
26  * - _Setter injection_: Each parameter of the instance configuration, that is
27  * not part of the constructor is injected through an appropriate setter
28  * method, if existing.
29  *
30  * To ensure that an instance implements a certain interface, the interface
31  * name may be passed together with the instance name to the DefaultFactory::addInterfaces
32  * method. The interfaces required by framework classes are already defined
33  * internally (see DefaultFactory::$requiredInterfaces).
34  *
35  * The following configuration shows the definition of the View instance:
36  *
37  * @code
38  * [View]
39  * __class = wcmf\lib\presentation\view\impl\SmartyView
40  * __shared = false
41  * compileCheck = true
42  * caching = false
43  * cacheLifetime = 3600
44  * cacheDir = app/cache/smarty/
45  *
46  * [HtmlView]
47  * __ref = $view
48  * @endcode
49  *
50  * In this example views are instances of _SmartyView_. They are not shared,
51  * meaning that clients get a fresh instance each time they request a view.
52  * The parameters compileCheck, caching, ... are either defined in the constructor
53  * of _SmartyView_ or there is an appropriate setter method (e.g.
54  * _SmartyView::setCompileCheck_). Any other constructor parameter of _SmartyView_
55  * is tried to resolve from either another instance defined in the configuration
56  * or an existing class. _HtmlView_ is defined as an alias for _View_.
57  *
58  * @author ingo herwig <ingo@wemove.com>
59  */
60 class DefaultFactory implements Factory {
61 
62  /**
63  * Interfaces that must be implemented by the given instances used
64  * in the framework.
65  */
66  protected $requiredInterfaces = [
67  'eventManager' => 'wcmf\lib\core\EventManager',
68  'logger' => 'wcmf\lib\core\Logger',
69  'logManager' => 'wcmf\lib\core\LogManager',
70  'session' => 'wcmf\lib\core\Session',
71  'configuration' => 'wcmf\lib\config\Configuration',
72  'localization' => 'wcmf\lib\i18n\Localization',
73  'message' => 'wcmf\lib\i18n\Message',
74  'cache' => 'wcmf\lib\io\Cache',
75  'persistenceFacade' => 'wcmf\lib\persistence\PersistenceFacade',
76  'transaction' => 'wcmf\lib\persistence\Transaction',
77  'concurrencyManager' => 'wcmf\lib\persistence\concurrency\ConcurrencyManager',
78  'actionMapper' => 'wcmf\lib\presentation\ActionMapper',
79  'request' => 'wcmf\lib\presentation\Request',
80  'response' => 'wcmf\lib\presentation\Response',
81  'listStrategies' => 'wcmf\lib\presentation\control\lists\ListStrategy',
82  'formats' => 'wcmf\lib\presentation\format\Format',
83  'formatter' => 'wcmf\lib\presentation\format\Formatter',
84  'view' => 'wcmf\lib\presentation\view\View',
85  'authenticationManager' => 'wcmf\lib\security\AuthenticationManager',
86  'permissionManager' => 'wcmf\lib\security\PermissionManager',
87  'principalFactory' => 'wcmf\lib\security\principal\PrincipalFactory',
88  'user' => 'wcmf\lib\security\principal\User',
89  'role' => 'wcmf\lib\security\principal\Role',
90  ];
91 
92  /**
93  * The registry for already created instances
94  */
95  protected $instances = [];
96 
97  /**
98  * The Configuration instance that holds the instance definitions
99  */
100  protected $configuration = null;
101 
102  protected $currentStack = [];
103 
104  /**
105  * Constructor.
106  * @param $configuration Configuration instance used to construct instances.
107  */
109  // TODO get additional interfaces from configuration
110  $this->configuration = $configuration;
111  }
112 
113  /**
114  * @see Factory::getInstance()
115  */
116  public function getInstance($name, $dynamicConfiguration=[]) {
117  $instance = null;
118  if (in_array($name, $this->currentStack)) {
119  throw new \Exception("Circular dependency detected: ".join(' - ', $this->currentStack)." - [".$name."]");
120  }
121  $this->currentStack[] = $name;
122 
123  // dynamic configuration must be included in internal instance key
124  $instanceKey = sizeof($dynamicConfiguration) == 0 ? strtolower($name) :
125  strtolower($name.json_encode($dynamicConfiguration));
126 
127  // check if the instance is registered already
128  if (isset($this->instances[$instanceKey])) {
129  $instance = $this->instances[$instanceKey];
130  }
131  else {
132  // construct instance
133  $staticConfiguration = $this->configuration->getSection($name, true);
134  $configuration = array_merge($staticConfiguration, $dynamicConfiguration);
135  $instance = $this->createInstance($name, $configuration, $instanceKey);
136  }
137  array_pop($this->currentStack);
138  return $instance;
139  }
140 
141  /**
142  * @see Factory::getNewInstance()
143  */
144  public function getNewInstance($name, $dynamicConfiguration=[]) {
145  $configuration = array_merge([
146  '__shared' => false
147  ], $dynamicConfiguration);
148 
149  $instance = $this->getInstance($name, $configuration);
150  return $instance;
151  }
152 
153  /**
154  * @see Factory::getInstanceOf()
155  */
156  public function getInstanceOf($class, $dynamicConfiguration=[]) {
157  $configuration = array_merge([
158  '__class' => $class,
159  '__shared' => false
160  ], $dynamicConfiguration);
161 
162  $instance = $this->createInstance($class, $configuration, null);
163  return $instance;
164  }
165 
166  /**
167  * @see Factory::registerInstance()
168  */
169  public function registerInstance($name, $instance) {
170  $instanceKey = strtolower($name);
171  $this->instances[$instanceKey] = $instance;
172  }
173 
174  /**
175  * @see Factory::addInterfaces()
176  */
177  public function addInterfaces(array $interfaces) {
178  $this->requiredInterfaces = array_merge($this->requiredInterfaces, $interfaces);
179  }
180 
181  /**
182  * @see Factory::clear()
183  */
184  public function clear() {
185  $this->instances = [];
186  }
187 
188  /**
189  * Get the setter method name for a property.
190  * @param $property
191  * @return String
192  */
193  protected function getSetterName($property) {
194  return 'set'.ucfirst(StringUtil::underScoreToCamelCase($property, true));
195  }
196 
197  /**
198  * Create an instance from the given configuration array.
199  * @param $name The name by which the instance may be retrieved later
200  * using Factory::getInstance(). The name is also used to check the interface
201  * (see Factory::addInterfaces())
202  * @param $configuration Associative array with key value pairs for instance properties
203  * passed to the constructor, setters or public properties and the special keys:
204  * - '__class': optional, denotes the instance class name
205  * - '__shared' optional, if true, the instance may be reused using Factory::getInstance())
206  * - '__ref' optional, defines this instance as an alias of the referenced instance
207  * @param $instanceKey The name under which the instance is registered for later retrieval
208  * @return Object
209  */
210  protected function createInstance($name, $configuration, $instanceKey) {
211  $instance = null;
212 
213  // return referenced instance, if the instance is an alias
214  if (isset($configuration['__ref'])) {
215  return $this->getInstance(preg_replace('/^\$/', '', $configuration['__ref']));
216  }
217 
218  // create instance
219  if (isset($configuration['__class'])) {
220  // the instance belongs to the given class
221  $className = $configuration['__class'];
222 
223  // class definition must be supplied by autoloader
224  if (class_exists($className)) {
225 
226  // collect constructor parameters
227  $cParams = [];
228  $refClass = new \ReflectionClass($className);
229  if ($refClass->hasMethod('__construct')) {
230  $refConstructor = new \ReflectionMethod($className, '__construct');
231  $refParameters = $refConstructor->getParameters();
232  foreach ($refParameters as $param) {
233  $paramName = $param->name;
234  $paramInstanceKey = strtolower($paramName);
235  if (isset($this->instances[$paramInstanceKey])) {
236  // parameter is already registered
237  $cParams[$paramName] = $this->instances[$paramInstanceKey];
238  }
239  elseif (isset($configuration[$paramName])) {
240  // parameter is explicitly defined in instance configuration
241  $cParams[$paramName] = $this->resolveValue($configuration[$paramName]);
242  }
243  elseif ($this->configuration->hasSection($paramName)) {
244  // parameter exists in configuration
245  $cParams[$paramName] = $this->getInstance($paramName);
246  }
247  elseif (($paramClass = $param->getClass()) != null) {
248  // check for parameter's class from type hint
249  // will cause an exception, if the class does not exist
250  $cParams[$paramName] = $this->getInstanceOf($paramClass->name);
251  }
252  elseif (!$param->isDefaultValueAvailable()) {
253  throw new ConfigurationException('Constructor parameter \''.$paramName.
254  '\' in class \''.$className.'\' cannot be injected.');
255  }
256  // delete resolved parameters from configuration
257  unset($configuration[$paramName]);
258  }
259  }
260 
261  // create the instance
262  $obj = $refClass->newInstanceArgs($cParams);
263 
264  // check against interface
265  $interface = $this->getInterface($name);
266  if ($interface != null && !($obj instanceof $interface)) {
267  throw new ConfigurationException('Class \''.$className.
268  '\' is required to implement interface \''.$interface.'\'.');
269  }
270 
271  // register the instance if it is shared (default)
272  // NOTE we do this before setting the instance properties in order
273  // to allow to resolve circular dependencies (dependent objects that
274  // are injected into the current instance via property injection can
275  // already use this instance)
276  if (!isset($configuration['__shared']) || $configuration['__shared'] == 'true') {
277  $this->registerInstance($instanceKey, $obj);
278  }
279 
280  // set the instance properties from the remaining configuration
281  foreach ($configuration as $key => $value) {
282  // exclude properties starting with __ and constructor parameters
283  if (strpos($key, '__') !== 0 && !isset($cParams[$key])) {
284  $value = $this->resolveValue($value);
285  // set the property
286  $setterName = $this->getSetterName($key);
287  if (method_exists($obj, $setterName)) {
288  $obj->$setterName($value);
289  }
290  else {
291  $obj->$key = $value;
292  }
293  }
294  }
295  $instance = $obj;
296  }
297  else {
298  throw new ConfigurationException('Class \''.$className.'\' is not found.');
299  }
300  }
301  else {
302  // the instance is a map
303 
304  // get interface that map values should implement
305  $interface = $this->getInterface($name);
306 
307  foreach ($configuration as $key => $value) {
308  // create instances for variables denoted by a leading $
309  if (strpos($value, '$') === 0) {
310  $obj = $this->getInstance(preg_replace('/^\$/', '', $value));
311  // check against interface
312  if ($interface != null && !($obj instanceof $interface)) {
313  throw new ConfigurationException('Class of \''.$name.'.'.$key.
314  '\' is required to implement interface \''.$interface.'\'.');
315  }
316  $configuration[$key] = $obj;
317  }
318  }
319  // always register maps
320  $this->registerInstance($instanceKey, $configuration);
321  $instance = $configuration;
322  }
323  return $instance;
324  }
325 
326  /**
327  * Resolve a configuration value into a parameter
328  * @param $value
329  * @return Mixed
330  */
331  protected function resolveValue($value) {
332  // special treatments, if value is a string
333  if (is_string($value)) {
334  // replace variables denoted by a leading $
335  if (strpos($value, '$') === 0) {
336  $value = $this->getInstance(preg_replace('/^\$/', '', $value));
337  }
338  else {
339  // convert booleans
340  $lower = strtolower($value);
341  if ($lower === 'true') {
342  $value = true;
343  }
344  if ($lower === 'false') {
345  $value = false;
346  }
347  }
348  }
349  // special treatments, if value is an array
350  if (is_array($value)) {
351  $result = [];
352  $containsInstance = false;
353  // check for variables
354  foreach ($value as $val) {
355  if (is_string($val) && strpos($val, '$') === 0) {
356  $result[] = $this->getInstance(preg_replace('/^\$/', '', $val));
357  $containsInstance = true;
358  }
359  else {
360  $result[] = $val;
361  }
362  }
363  // only replace value, if the array containes an variable
364  if ($containsInstance) {
365  $value = $result;
366  }
367  }
368  return $value;
369  }
370 
371  /**
372  * Get the interface that is required to be implemented by the given instance
373  * @param $name The name of the instance
374  * @return Interface or null, if undefined
375  */
376  protected function getInterface($name) {
377  if (isset($this->requiredInterfaces[$name])) {
378  return $this->requiredInterfaces[$name];
379  }
380  return null;
381  }
382 }
383 ?>
Interface for Factory implementations.
Definition: Factory.php:19
__construct(Configuration $configuration)
Constructor.
Implementations of Configuration give access to the application configuration.
ConfigurationException signals an exception in the configuration.
$configuration
The Configuration instance that holds the instance definitions.
getSetterName($property)
Get the setter method name for a property.
getNewInstance($name, $dynamicConfiguration=[])
getInstanceOf($class, $dynamicConfiguration=[])
$instances
The registry for already created instances.
createInstance($name, $configuration, $instanceKey)
Create an instance from the given configuration array.
getInstance($name, $dynamicConfiguration=[])
$requiredInterfaces
Interfaces that must be implemented by the given instances used in the framework.
DefaultFactory is used to create service instances.
resolveValue($value)
Resolve a configuration value into a parameter.