EmbeDi::addConfig()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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