Completed
Push — master ( 4ea525...0da2a9 )
by Peter
06:28
created

EmbeDi::__construct()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

Changes 11
Bugs 1 Features 2
Metric Value
c 11
b 1
f 2
dl 0
loc 28
ccs 19
cts 19
cp 1
rs 8.5806
cc 4
eloc 13
nc 8
nop 3
crap 4
1
<?php
2
3
/**
4
 * This software package is licensed under `AGPL, Commercial` license[s].
5
 *
6
 * @package maslosoft/embedi
7
 * @license AGPL, Commercial
8
 *
9
 * @copyright Copyright (c) Peter Maselkowski <[email protected]>
10
 *
11
 */
12
13
namespace Maslosoft\EmbeDi;
14
15
use InvalidArgumentException;
16
use Maslosoft\EmbeDi\Interfaces\AdapterInterface;
17
use Maslosoft\EmbeDi\Managers\SourceManager;
18
use Maslosoft\EmbeDi\Storage\EmbeDiStore;
19
use ReflectionObject;
20
use ReflectionProperty;
21
22
/**
23
 * Embedded dependency injection container
24
 *
25
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
26
 */
27
class EmbeDi
28
{
29
30
	/**
31
	 * This is default instance name, and component name.
32
	 */
33
	const DefaultInstanceId = 'embedi';
0 ignored issues
show
Coding Style introduced by
Constant DefaultInstanceId should be defined in uppercase
Loading history...
34
35
	/**
36
	 * Class field in configuration arrays
37
	 * @see apply()
38
	 * @see export()
39
	 * @var string
40
	 */
41
	public $classField = 'class';
42
43
	/**
44
	 * Instance id
45
	 * @var string
46
	 */
47
	private $_instanceId = '';
48
49
	/**
50
	 * Preset ID
51
	 * @var string
52
	 */
53
	private $_presetId = '';
54
55
	/**
56
	 * Storage container
57
	 * @var EmbeDiStore
58
	 */
59
	private $storage = null;
60
61
	/**
62
	 * Configs source manager
63
	 * @var SourceManager
64
	 */
65
	private $sm = null;
66
67
	/**
68
	 * Flyweight instances of EmbeDi
69
	 * @var EmbeDi[]
70
	 */
71
	private static $_instances = [];
72
73
	/**
74
	 * Create container with provided id
75
	 * @param string $instanceId
76
	 * @param string $presetId If set will lookup configuration in depper array level
77
	 * @param array $config Configuration of EmbeDi
78
	 */
79 17
	public function __construct($instanceId = EmbeDi::DefaultInstanceId, $presetId = null, $config = [])
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
80
	{
81 17
		$this->_instanceId = $instanceId;
82 17
		$this->_presetId = $presetId;
83 17
		if (!empty($config))
84 17
		{
85 1
			$this->apply($config, $this);
86 1
		}
87 17
		$this->storage = new EmbeDiStore(__CLASS__, EmbeDiStore::StoreId);
88
89
		/**
90
		 * TODO Pass $this as second param
91
		 */
92 17
		$this->sm = new SourceManager($instanceId, $presetId);
93 17
		if (!empty($presetId))
94 17
		{
95 1
			$key = $instanceId . '.' . $presetId;
96 1
		}
97
		else
98
		{
99 16
			$key = $instanceId;
100
		}
101
		// Assign flyweight instance
102 17
		if (empty(self::$_instances[$key]))
103 17
		{
104 5
			self::$_instances[$key] = $this;
105 5
		}
106 17
	}
107
108 2
	public function __get($name)
109
	{
110 2
		$methodName = sprintf('get%s', ucfirst($name));
111 2
		return $this->{$methodName}();
112
	}
113
114 3
	public function __set($name, $value)
115
	{
116 3
		$methodName = sprintf('set%s', ucfirst($name));
117 3
		return $this->{$methodName}($value);
118
	}
119
120
	/**
121
	 * Get flyweight instance of embedi.
122
	 * This will create instance only if `$instanceId` insntace id does not exists.
123
	 * If named instance exists, or was ever create - existing instance will be used.
124
	 * Use this function especially when require many `EmbeDi` calls,
125
	 * for instance when creating `EmbeDi` in loops:
126
	 * ```php
127
	 * foreach($configs as $config)
128
	 * {
129
	 * 		(new EmbeDi)->apply($config);
130
	 * }
131
	 * ```
132
	 * In abowe example at each loop iteration new `EmbeDi` instance is created.
133
	 * While it is still lightweight, it's unnessesary overhead.
134
	 *
135
	 * This can be made in slightly more optimized way by using `fly` function:
136
	 * ```php
137
	 * foreach($configs as $config)
138
	 * {
139
	 * 		EmbeDi::fly()->apply($config);
140
	 * }
141
	 * ```
142
	 * In above example only one instance of `EmbeDi` is used.
143
	 * @param string $instanceId
144
	 * @return EmbeDi
145
	 */
146 4
	public static function fly($instanceId = EmbeDi::DefaultInstanceId, $presetId = null)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
147
	{
148 4
		if (!empty($presetId))
149 4
		{
150
			$key = $instanceId . '.' . $presetId;
151
		}
152
		else
153
		{
154 4
			$key = $instanceId;
155
		}
156 4
		if (empty(self::$_instances[$key]))
157 4
		{
158 1
			self::$_instances[$key] = new static($key);
159 1
		}
160 4
		return self::$_instances[$key];
161
	}
162
163 2
	public function getAdapters()
164
	{
165 2
		return $this->storage->adapters;
166
	}
167
168
	/**
169
	 * TODO Create AdaptersManager
170
	 */
171 7
	public function setAdapters($adapters)
172
	{
173 7
		$instances = [];
174 7
		foreach ($adapters as $adapter)
175
		{
176
			// Assuming class name
177 7
			if (is_string($adapter))
178 7
			{
179 1
				$instances[] = new $adapter;
180 1
				continue;
181
			}
182
			// Set directly
183 7
			if ($adapter instanceof AdapterInterface)
184 7
			{
185 7
				$instances[] = $adapter;
186 7
				continue;
187
			}
188
			else
189
			{
190 1
				throw new InvalidArgumentException(sprintf('Adapter of `%s->adapters` is of type `%s`, string (class name) or `%s` required', __CLASS__, gettype($adapter) == 'object' ? get_class($adapter) : gettype($adapter), AdapterInterface::class));
191
			}
192 7
		}
193 7
		$this->storage->adapters = $instances;
194 7
		return $this;
195
	}
196
197
	/**
198
	 * Add configuration adapter
199
	 * TODO Create AdaptersManager
200
	 * @param AdapterInterface $adapter
201
	 */
202 3
	public function addAdapter(AdapterInterface $adapter)
203
	{
204 3
		$this->storage->adapters[] = $adapter;
205 3
	}
206
207
	/**
208
	 * Add configuration source for later use
209
	 * Config should have keys of component id and values of config.
210
	 * Example:
211
	 * ```
212
	 * [
213
	 * 		'logger' => [
214
	 * 			'class' => Monolog\Logger\Logger,
215
	 * 		],
216
	 * 		'mangan' => [
217
	 * 			'@logger' => 'logger'
218
	 * 		]
219
	 * ]
220
	 * ```
221
	 * Attributes starting with `@` denotes that link to other
222
	 * config component should be used. In example above, mangan field `logger`
223
	 * will be configured with monolog logger.
224
	 * @deprecated Use Maslosoft\EmbeDi\Adapters\ArrayAdapter instead
225
	 * @param mixed[] $source
226
	 */
227
	public function addConfig($source)
228
	{
229
		$this->sm->add($source);
230
	}
231
232
	/**
233
	 * Check whenever current configuration is stored.
234
	 * @return bool
235
	 */
236 16
	public function isStored($object)
237
	{
238 16
		return (new DiStore($object, $this->_instanceId, $this->_presetId))->stored;
239
	}
240
241
	/**
242
	 * Configure existing object from previously stored configuration.
243
	 * Typically this will will be called in your class constructor.
244
	 * Will try to find configuration in adapters if it's not stored.
245
	 * TODO Use SourceManager here, before adapters
246
	 * TODO Create AdaptersManager and use here
247
	 * @param object $object
248
	 * @return object
249
	 */
250 16
	public function configure($object)
251
	{
252 16
		$storage = new DiStore($object, $this->_instanceId, $this->_presetId);
253
254
		// Only configure if stored
255 16
		if ($this->isStored($object))
256 16
		{
257
			/**
258
			 * TODO Use apply() here
259
			 */
260 2
			foreach ($storage->data as $name => $value)
261
			{
262 2
				$class = $storage->classes[$name];
263
				if ($class)
264 2
				{
265 1
					$object->$name = new $class;
266 1
					$this->configure($object->$name);
267 1
				}
268
				else
269
				{
270 2
					$object->$name = $value;
271
				}
272 2
			}
273 2
			return;
274
		}
275
276
		// Try to find configuration in adapters
277 16
		foreach ($this->storage->adapters as $adapter)
278
		{
279 8
			$config = $adapter->getConfig(get_class($object), $this->_instanceId, $this->_presetId);
280
			if ($config)
281 8
			{
282 7
				$this->apply($config, $object);
283 7
				return;
284
			}
285 9
		}
286 9
	}
287
288
	/**
289
	 * Apply configuration to object from array.
290
	 *
291
	 * This can also create object if passed configuration array have `class` field.
292
	 *
293
	 * Example of creating object:
294
	 * ```
295
	 * $config = [
296
	 * 		'class' => Vendor\Component::class,
297
	 * 		'title' => 'bar'
298
	 * ];
299
	 * (new Embedi)->apply($config);
300
	 * ```
301
	 *
302
	 * Example of applying config to existing object:
303
	 * ```
304
	 * $config = [
305
	 * 		'title' => 'bar'
306
	 * ];
307
	 * (new Embedi)->apply($config, new Vendor\Component);
308
	 * ```
309
	 *
310
	 * If `$configuration` arguments is string, it will simply instantiate class:
311
	 * ```
312
	 * (new Embedi)->apply('Vendor\Package\Component');
313
	 * ```
314
	 *
315
	 * @param string|mixed[][] $configuration
316
	 * @param object $object Object to configure, set to null to create new one
317
	 * @return object
318
	 */
319 10
	public function apply($configuration, $object = null)
320
	{
321 10
		if (is_string($configuration))
322 10
		{
323
			return new $configuration;
324
		}
325 10
		if (null === $object && array_key_exists($this->classField, $configuration))
326 10
		{
327 10
			$className = $configuration[$this->classField];
328 10
			unset($configuration[$this->classField]);
329 10
			$object = new $className;
330 10
		}
331 10
		foreach ($configuration as $name => $value)
332
		{
333 10
			if ($name === $this->classField)
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $name (integer) and $this->classField (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
334 10
			{
335 7
				continue;
336
			}
337 10
			if (strpos($name, '@') === 0)
338 10
			{
339 1
				$name = substr($name, 1);
340 1
				$object->$name = $this->sm->get($value);
341 1
				continue;
342
			}
343 10
			if (is_array($value) && array_key_exists($this->classField, $value))
344 10
			{
345 9
				$object->$name = $this->apply($value);
346 9
			}
347
			else
348
			{
349 10
				$object->$name = $value;
350
			}
351 10
		}
352 10
		return $object;
353
	}
354
355
	/**
356
	 * Export object configuration to array
357
	 * @param object $object
358
	 * @param string[] $fields
359
	 * @return mixed[][]
360
	 */
361 4
	public function export($object, $fields = [])
362
	{
363 4
		$data = [];
364 4
		foreach ($this->_getFields($object, $fields) as $name)
365
		{
366
			// If object, recurse
367 4
			if (!isset($object->$name))
368 4
			{
369 3
				continue;
370
			}
371 4
			if (is_object($object->$name))
372 4
			{
373 1
				$data[$name] = $this->export($object->$name);
374 1
			}
375
			else
376
			{
377 4
				$data[$name] = $object->$name;
378
			}
379 4
		}
380 4
		$data[$this->classField] = get_class($object);
381 4
		return $data;
382
	}
383
384
	/**
385
	 * Store object configuration.
386
	 *
387
	 * This will be typically called in init method of your component.
388
	 * After storing config, configuration will be available in `configure` method.
389
	 * `configure` method should be called in your class constructor.
390
	 *
391
	 * If you store config and have `configure` method call,
392
	 * after subsequent creations of your component will be configured by EmbeDi.
393
	 *
394
	 * Both methods could be called in constructor, if you don't need additional
395
	 * initialization code after configuring object.
396
	 *
397
	 * Example workflow:
398
	 * ```
399
	 * class Component
400
	 * {
401
	 * 		public $title = '';
402
	 *
403
	 * 		public function __construct()
404
	 * 		{
405
	 * 			(new EmbeDi)->configure($this);
406
	 * 		}
407
	 *
408
	 * 		public function init()
409
	 * 		{
410
	 * 			(new EmbeDi)->store($this);
411
	 * 		}
412
	 * }
413
	 *
414
	 * $c1 = new Component();
415
	 * $c1->title = 'foo';
416
	 * $c1->init();
417
	 *
418
	 * $c2 = new Component();
419
	 *
420
	 * echo $c2->title; // 'foo'
421
	 * ```
422
	 *
423
	 * Parameter `$fields` tell's EmbeDi to store only subset of class fields.
424
	 * Example:
425
	 * ```
426
	 * (new EmbeDi)->store($this, ['title']);
427
	 * ```
428
	 *
429
	 * Parameter `$update` tell's EmbeDi to update existing configuration.
430
	 * By default configuration is not ovveriden on subsequent `store` calls.
431
	 * This is done on purpose, to not mess basic configuration.
432
	 *
433
	 * @param object $object Object to store
434
	 * @param string[] $fields Fields to store
435
	 * @param bool $update Whenever to update existing configuration
436
	 * @return mixed[] Stored data
437
	 */
438 7
	public function store($object, $fields = [], $update = false)
439
	{
440 7
		$storage = new DiStore($object, $this->_instanceId);
441
442
		// Do not modify stored instance
443 7
		if ($this->isStored($object) && !$update)
444 7
		{
445 1
			return $storage;
446
		}
447
448 7
		$data = [];
449 7
		$classes = [];
450 7
		foreach ($this->_getFields($object, $fields) as $name)
451
		{
452
			// If object, recurse
453 7
			if (is_object($object->$name))
454 7
			{
455 1
				$data[$name] = $this->store($object->$name);
456 1
				$classes[$name] = get_class($object->$name);
457 1
			}
458
			else
459
			{
460 7
				$data[$name] = $object->$name;
461 7
				$classes[$name] = '';
462
			}
463 7
		}
464 7
		$storage->stored = true;
465 7
		$storage->data = $data;
466 7
		$storage->classes = $classes;
467 7
		$storage->class = get_class($object);
468 7
		return $data;
469
	}
470
471
	/**
472
	 * Get class fields of object. By default all public and non static fields are returned.
473
	 * This can be overridden by passing `$fields` names of fields. These are not checked for existence.
474
	 * @param object $object
475
	 * @param string[] $fields
476
	 * @return string[]
477
	 */
478 8
	private function _getFields($object, $fields)
479
	{
480 8
		if (empty($fields))
481 8
		{
482 8
			foreach ((new ReflectionObject($object))->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
483
			{
484
				// http://stackoverflow.com/a/15784768/133408
485 8
				if (!$property->isStatic())
486 8
				{
487 8
					$fields[] = $property->name;
488 8
				}
489 8
			}
490 8
		}
491 8
		return $fields;
492
	}
493
494
}
495