Completed
Push — master ( c632d0...4a7120 )
by Peter
23:08
created

Signal::getConfigured()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 8
nc 4
nop 1
crap 3
1
<?php
2
3
/**
4
 * This software package is licensed under `AGPL, Commercial` license[s].
5
 *
6
 * @package maslosoft/signals
7
 * @license AGPL, Commercial
8
 *
9
 * @copyright Copyright (c) Peter Maselkowski <[email protected]>
10
 *
11
 */
12
13
namespace Maslosoft\Signals;
14
15
use Maslosoft\Addendum\Utilities\ClassChecker;
16
use Maslosoft\Addendum\Utilities\NameNormalizer;
17
use Maslosoft\Cli\Shared\ConfigReader;
18
use Maslosoft\EmbeDi\EmbeDi;
19
use Maslosoft\Signals\Builder\Addendum;
20
use Maslosoft\Signals\Builder\IO\PhpFile;
21
use Maslosoft\Signals\Factories\FilterFactory;
22
use Maslosoft\Signals\Factories\SlotFactory;
23
use Maslosoft\Signals\Helpers\PostFilter;
24
use Maslosoft\Signals\Helpers\PreFilter;
25
use Maslosoft\Signals\Interfaces\BuilderIOInterface;
26
use Maslosoft\Signals\Interfaces\ExtractorInterface;
27
use Maslosoft\Signals\Interfaces\FilterInterface;
28
use Maslosoft\Signals\Interfaces\PostFilterInterface;
29
use Maslosoft\Signals\Interfaces\PreFilterInterface;
30
use Maslosoft\Signals\Interfaces\SignalAwareInterface;
31
use Psr\Log\LoggerAwareInterface;
32
use Psr\Log\LoggerInterface;
33
use Psr\Log\NullLogger;
34
use ReflectionClass;
35
use UnexpectedValueException;
36
37
/**
38
 * Main signals components
39
 *
40
 * @author Piotr
41
 * @property LoggerInterface $logger Logger, set this to log warnings, notices errors. This is shorthand for `get/setLogger`.
42
 */
43
class Signal implements LoggerAwareInterface
44
{
45
46
	const Slots = 'slots';
47
	const Signals = 'signals';
48
49
	/**
50
	 * Generated signals name.
51
	 * Name of this constant is confusing.
52
	 * @internal description
53
	 */
54
	const ConfigFilename = 'signals-definition.php';
55
56
	/**
57
	 * Config file name
58
	 */
59
	const ConfigName = "signals";
60
61
	/**
62
	 * Runtime path.
63
	 * This is path where config from yml will be stored.
64
	 * Path is relative to project root.
65
	 * @var string
66
	 */
67
	public $runtimePath = 'runtime';
68
69
	/**
70
	 * This aliases will be searched for SlotFor and SignalFor annotations
71
	 * TODO Autodetect based on composer autoload
72
	 * @var string[]
73
	 */
74
	public $paths = [
75
		'vendor',
76
	];
77
78
	/**
79
	 * Directories to ignore while scanning
80
	 * @var string[]
81
	 */
82
	public $ignoreDirs = [
83
		'vendor', // Vendors in vendors
84
		'generated', // Generated data, including signals
85
		'runtime', // Runtime data
86
	];
87
88
	/**
89
	 * Filters configuration.
90
	 * This filters will be applied to every emit.
91
	 * @var string[]|object[]
92
	 */
93
	public $filters = [];
94
95
	/**
96
	 * Sorters configuration.
97
	 * @var string[]|object[]
98
	 */
99
	public $sorters = [];
100
101
	/**
102
	 * Extractor configuration
103
	 * @var string|[]|object
104
	 */
105
	public $extractor = Addendum::class;
106
107
	/**
108
	 * Input/Output configuration
109
	 * @var string|[]|object
110
	 */
111
	public $io = PhpFile::class;
112
113
	/**
114
	 * Whenever component is initialized
115
	 * @var bool
116
	 */
117
	public $isInitialized = false;
118
119
	/**
120
	 * Configuration of signals and slots
121
	 * @var string[][]
122
	 */
123
	private static $config = [];
124
125
	/**
126
	 * Logger instance holder
127
	 * NOTE: There is property annotation with `logger` name,
128
	 * thus this name is a bit longer
129
	 * @var LoggerInterface
130
	 */
131
	private $loggerInstance = null;
132
133
	/**
134
	 * Embedded dependency injection
135
	 * @var EmbeDi
136
	 */
137
	private $di = null;
138
139
	/**
140
	 * Version
141
	 * @var string
142
	 */
143
	private $version = null;
144
145
	/**
146
	 * Current filters
147
	 * @var PreFilterInterface[]|PostFilterInterface[]
148
	 */
149
	private $currentFilters = [];
150
151 17
	public function __construct($configName = self::ConfigName)
152
	{
153 17
		$this->loggerInstance = new NullLogger;
154
155
		/**
156
		 * TODO This should be made as embedi adapter, currently unsupported
157
		 */
158 17
		$config = new ConfigReader($configName);
159 17
		$this->di = EmbeDi::fly($configName);
160 17
		$this->di->configure($this);
161 17
		$this->di->apply($config->toArray(), $this);
162 17
	}
163
164
	/**
165
	 * Getter
166
	 * @param string $name
167
	 * @return mixed
168
	 */
169
	public function __get($name)
170
	{
171
		return $this->{'get' . ucfirst($name)}();
172
	}
173
174
	/**
175
	 * Setter
176
	 * @param string $name
177
	 * @param mixed $value
178
	 * @return mixed
179
	 */
180
	public function __set($name, $value)
181
	{
182
		return $this->{'set' . ucfirst($name)}($value);
183
	}
184
185
	/**
186
	 * Get current signals version
187
	 *
188
	 * @codeCoverageIgnore
189
	 * @return string
190
	 */
191
	public function getVersion()
192
	{
193
		if (null === $this->version)
194
		{
195
			$this->version = require __DIR__ . '/version.php';
196
		}
197
		return $this->version;
198
	}
199
200
	public function init()
201
	{
202
		if (!$this->isInitialized)
203
		{
204
			$this->reload();
205
		}
206
		if (!$this->di->isStored($this))
207
		{
208
			$this->di->store($this);
209
		}
210
	}
211
212
	/**
213
	 * Apply filter to current emit.
214
	 * @param FilterInterface|string|mixed $filter
215
	 * @return Signal
216
	 * @throws UnexpectedValueException
217
	 */
218 3
	public function filter($filter)
219
	{
220
		// Instantiate from string or array
221 3
		if (!is_object($filter))
222
		{
223 1
			$filter = $this->di->apply($filter);
224
		}
225 3
		if (!$filter instanceof PreFilterInterface && !$filter instanceof PostFilterInterface)
226
		{
227
			throw new UnexpectedValueException(sprintf('$filter must implement either `%s` or `%s` interface', PreFilterInterface::class, PostFilterInterface::class));
228
		}
229 3
		$this->currentFilters[] = $filter;
230 3
		return $this;
231
	}
232
233
	/**
234
	 * Emit signal to inform slots
235
	 * @param object|string $signal
236
	 * @return object[]
237
	 */
238 16
	public function emit($signal)
239
	{
240 16
		$result = [];
241 16
		if (is_string($signal))
242
		{
243
			$signal = new $signal;
244
		}
245 16
		$name = get_class($signal);
246 16
		NameNormalizer::normalize($name);
247 16
		if (empty(self::$config))
248
		{
249
			$this->init();
250
		}
251 16
		if (!isset(self::$config[self::Signals][$name]))
252
		{
253
			self::$config[self::Signals][$name] = [];
254
			$this->loggerInstance->debug('No slots found for signal `{name}`, skipping', ['name' => $name]);
255
			return $result;
256
		}
257
258 16
		foreach (self::$config[self::Signals][$name] as $fqn => $injections)
0 ignored issues
show
Bug introduced by
The expression self::$config[self::Signals][$name] of type string is not traversable.
Loading history...
259
		{
260
			// Skip
261 16
			if (false === $injections || count($injections) == 0)
262
			{
263
				continue;
264
			}
265 16
			if (!PreFilter::filter($this, $fqn, $signal))
266
			{
267 3
				continue;
268
			}
269 13
			foreach ($injections as $injection)
270
			{
271 13
				$injected = SlotFactory::create($this, $signal, $fqn, $injection);
272 13
				if (false === $injected)
273
				{
274
					continue;
275
				}
276 13
				if (!PostFilter::filter($this, $injected, $signal))
277
				{
278 2
					continue;
279
				}
280 13
				$result[] = $injected;
281
			}
282
		}
283 16
		$this->currentFilters = [];
284 16
		return $result;
285
	}
286
287
	/**
288
	 * Call for signals from slot
289
	 * @param object $slot
290
	 * @param string $interface Interface or class name which must be implemented, instanceof or sub class of to get into slot
291
	 */
292
	public function gather($slot, $interface = null)
293
	{
294
		$name = get_class($slot);
295
		NameNormalizer::normalize($name);
296
		if (!empty($interface))
297
		{
298
			NameNormalizer::normalize($interface);
299
		}
300
		if (empty(self::$config))
301
		{
302
			$this->init();
303
		}
304
		if (!isset(self::$config[self::Slots][$name]))
305
		{
306
			self::$config[self::Slots][$name] = [];
307
			$this->loggerInstance->debug('No signals found for slot `{name}`, skipping', ['name' => $name]);
308
		}
309
		$result = [];
310
		foreach ((array) self::$config[self::Slots][$name] as $fqn => $emit)
311
		{
312
			if (false === $emit)
313
			{
314
				continue;
315
			}
316
			// Check if class exists and log if doesn't
317
			if (!ClassChecker::exists($fqn))
318
			{
319
				$this->loggerInstance->debug(sprintf("Class `%s` not found while gathering slot `%s`", $fqn, get_class($slot)));
320
				continue;
321
			}
322
			if (null === $interface)
323
			{
324
				$result[] = new $fqn;
325
				continue;
326
			}
327
328
			// Check if it's same as interface
329
			if ($fqn === $interface)
330
			{
331
				$result[] = new $fqn;
332
				continue;
333
			}
334
335
			// Check if class implements interface
336
			$info = new ReflectionClass($fqn);
337
			if ($info->implementsInterface($interface))
338
			{
339
				$result[] = new $fqn;
340
				continue;
341
			}
342
343
			// Check if class is instance of interface
344
			if ($info->isSubclassOf($interface))
345
			{
346
				$result[] = new $fqn;
347
				continue;
348
			}
349
		}
350
		return $result;
351
	}
352
353
	/**
354
	 * Get filters
355
	 * @param string $interface
356
	 * @return PreFilterInterface[]|PostFilterInterface[]
357
	 */
358 16
	public function getFilters($interface)
359
	{
360 16
		$filters = FilterFactory::create($this, $interface);
361 16
		foreach ($this->currentFilters as $filter)
362
		{
363 3
			if (!$filter instanceof $interface)
364
			{
365 1
				continue;
366
			}
367 3
			$filters[] = $filter;
368
		}
369 16
		return $filters;
370
	}
371
372
	/**
373
	 * Get logger
374
	 * @codeCoverageIgnore
375
	 * @return LoggerInterface
376
	 */
377
	public function getLogger()
378
	{
379
		return $this->loggerInstance;
380
	}
381
382
	/**
383
	 * Set logger
384
	 * @codeCoverageIgnore
385
	 * @param LoggerInterface $logger
386
	 * @return Signal
387
	 */
388
	public function setLogger(LoggerInterface $logger)
389
	{
390
		$this->loggerInstance = $logger;
391
		return $this;
392
	}
393
394
	/**
395
	 * Get dependency injection container
396
	 * @return EmbeDi
397
	 */
398 16
	public function getDi()
399
	{
400 16
		return $this->di;
401
	}
402
403
	public function setDi(EmbeDi $di)
404
	{
405
		$this->di = $di;
406
		return $this;
407
	}
408
409
	/**
410
	 * Get Input/Output adapter
411
	 * @codeCoverageIgnore
412
	 * @return BuilderIOInterface I/O Adapter
413
	 */
414
	public function getIO()
415
	{
416
		return $this->getConfigured('io');
417
	}
418
419
	/**
420
	 * Set Input/Output interface
421
	 * @codeCoverageIgnore
422
	 * @param BuilderIOInterface $io
423
	 * @return Signal
424
	 */
425
	public function setIO(BuilderIOInterface $io)
426
	{
427
		return $this->setConfigured($io, 'io');
428
	}
429
430
	/**
431
	 * @codeCoverageIgnore
432
	 * @return ExtractorInterface
433
	 */
434
	public function getExtractor()
435
	{
436
		return $this->getConfigured('extractor');
437
	}
438
439
	/**
440
	 * @codeCoverageIgnore
441
	 * @param ExtractorInterface $extractor
442
	 * @return Signal
443
	 */
444
	public function setExtractor(ExtractorInterface $extractor)
445
	{
446
		return $this->setConfigured($extractor, 'extractor');
447
	}
448
449
	/**
450
	 * Reloads signals cache and reinitializes component.
451
	 */
452
	public function resetCache()
453
	{
454
		$this->reload();
455
	}
456
457
	private function reload()
458
	{
459
		self::$config = $this->getIO()->read();
460
	}
461
462
	/**
463
	 * Get configured property
464
	 * @param string $property
465
	 * @return SignalAwareInterface
466
	 */
467 1
	private function getConfigured($property)
468
	{
469 1
		if (is_object($this->$property))
470
		{
471 1
			$object = $this->$property;
472
		}
473
		else
474
		{
475
			$object = $this->di->apply($this->$property);
476
		}
477 1
		if ($object instanceof SignalAwareInterface)
478
		{
479 1
			$object->setSignal($this);
480
		}
481 1
		return $object;
482
	}
483
484
	/**
485
	 * Set signal aware property
486
	 * @param SignalAwareInterface $object
487
	 * @param string $property
488
	 * @return Signal
489
	 */
490
	private function setConfigured(SignalAwareInterface $object, $property)
491
	{
492
		$object->setSignal($this);
493
		$this->$property = $object;
494
		return $this;
495
	}
496
497
}
498