Completed
Push — master ( ee429b...2d69aa )
by Peter
03:45
created

Signal::filter()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.3541

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 20
ccs 11
cts 14
cp 0.7856
rs 8.8571
cc 6
eloc 10
nc 5
nop 1
crap 6.3541
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 25
	public function __construct($configName = self::ConfigName)
152
	{
153 25
		$this->loggerInstance = new NullLogger;
154
155
		/**
156
		 * TODO This should be made as embedi adapter, currently unsupported
157
		 */
158 25
		$config = new ConfigReader($configName);
159 25
		$this->di = EmbeDi::fly($configName);
160 25
		$this->di->configure($this);
161 25
		$this->di->apply($config->toArray(), $this);
162 25
	}
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
	 *
215
	 * Pass false as param to disable all filters.
216
	 *
217
	 * @param FilterInterface|string|mixed $filter
218
	 * @return Signal
219
	 * @throws UnexpectedValueException
220
	 */
221 4
	public function filter($filter)
222
	{
223
		// disable filters
224 4
		if (is_bool($filter) && false === $filter)
225 4
		{
226
			$this->currentFilters = [];
227
			return $this;
228
		}
229
		// Instantiate from string or array
230 4
		if (!is_object($filter))
231 4
		{
232 1
			$filter = $this->di->apply($filter);
233 1
		}
234 4
		if (!$filter instanceof PreFilterInterface && !$filter instanceof PostFilterInterface)
235 4
		{
236
			throw new UnexpectedValueException(sprintf('$filter must implement either `%s` or `%s` interface', PreFilterInterface::class, PostFilterInterface::class));
237
		}
238 4
		$this->currentFilters[] = $filter;
239 4
		return $this;
240
	}
241
242
	/**
243
	 * Emit signal to inform slots
244
	 * @param object|string $signal
245
	 * @return object[]
246
	 */
247 24
	public function emit($signal)
248
	{
249 17
		$result = [];
250 17
		if (is_string($signal))
251 17
		{
252
			$signal = new $signal;
253
		}
254 17
		$name = get_class($signal);
255 17
		NameNormalizer::normalize($name);
256 17
		if (empty(self::$config))
257 17
		{
258
			$this->init();
259
		}
260 24
		if (!isset(self::$config[self::Signals][$name]))
261 17
		{
262 22
			self::$config[self::Signals][$name] = [];
263
			$this->loggerInstance->debug('No slots found for signal `{name}`, skipping', ['name' => $name]);
264
			return $result;
265
		}
266
267 17
		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...
268
		{
269
			// Skip
270 17
			if (false === $injections || count($injections) == 0)
271 17
			{
272
				continue;
273
			}
274 17
			if (!PreFilter::filter($this, $fqn, $signal))
275 17
			{
276 3
				continue;
277 3
			}
278 14
			foreach ($injections as $injection)
279
			{
280 14
				$injected = SlotFactory::create($this, $signal, $fqn, $injection);
281 14
				if (false === $injected)
282 14
				{
283
					continue;
284
				}
285 14
				if (!PostFilter::filter($this, $injected, $signal))
286 14
				{
287 2
					continue;
288
				}
289 12
				$result[] = $injected;
290 14
			}
291 17
		}
292 17
		$this->currentFilters = [];
293 17
		return $result;
294
	}
295
296
	/**
297
	 * Call for signals from slot
298
	 * @param object $slot
299
	 * @param string $interface Interface or class name which must be implemented, instanceof or sub class of to get into slot
300
	 */
301 7
	public function gather($slot, $interface = null)
302
	{
303 7
		$name = get_class($slot);
304 7
		NameNormalizer::normalize($name);
305 7
		if (!empty($interface))
306 7
		{
307 3
			NameNormalizer::normalize($interface);
308 3
		}
309 7
		if (empty(self::$config))
310 7
		{
311
			$this->init();
312
		}
313 7
		if (!isset(self::$config[self::Slots][$name]))
314 7
		{
315
			self::$config[self::Slots][$name] = [];
316
			$this->loggerInstance->debug('No signals found for slot `{name}`, skipping', ['name' => $name]);
317
		}
318 7
		$result = [];
319 7
		foreach ((array) self::$config[self::Slots][$name] as $fqn => $emit)
320
		{
321 7
			if (false === $emit)
322 7
			{
323
				continue;
324
			}
325 7
			if (!PreFilter::filter($this, $fqn, $slot))
326 7
			{
327
				continue;
328
			}
329
			// Check if class exists and log if doesn't
330 7
			if (!ClassChecker::exists($fqn))
331 7
			{
332
				$this->loggerInstance->debug(sprintf("Class `%s` not found while gathering slot `%s`", $fqn, get_class($slot)));
333
				continue;
334
			}
335 7
			if (null === $interface)
336 7
			{
337 4
				$injected = new $fqn;
338 4
				if (!PostFilter::filter($this, $injected, $slot))
339 4
				{
340 2
					continue;
341
				}
342 2
				$result[] = $injected;
343 2
				continue;
344
			}
345
346
			// Check if it's same as interface
347 3
			if ($fqn === $interface)
348 3
			{
349 1
				$injected = new $fqn;
350 1
				if (!PostFilter::filter($this, $injected, $slot))
351 1
				{
352
					continue;
353
				}
354 1
				$result[] = $injected;
355 1
				continue;
356
			}
357
358 3
			$info = new ReflectionClass($fqn);
359
360
			// Check if class is instance of base class
361 3
			if ($info->isSubclassOf($interface))
362 3
			{
363 2
				$injected = new $fqn;
364 2
				if (!PostFilter::filter($this, $injected, $slot))
365 2
				{
366
					continue;
367
				}
368 2
				$result[] = $injected;
369 2
				continue;
370
			}
371
372 3
			$interfaceInfo = new ReflectionClass($interface);
373
			// Check if class implements interface
374 3
			if ($interfaceInfo->isInterface() && $info->implementsInterface($interface))
375 3
			{
376
				$injected = new $fqn;
377
				if (!PostFilter::filter($this, $injected, $slot))
378
				{
379
					continue;
380
				}
381
				$result[] = $injected;
382
				continue;
383
			}
384 7
		}
385 7
		return $result;
386
	}
387
388
	/**
389
	 * Get filters
390
	 * @param string $interface
391
	 * @return PreFilterInterface[]|PostFilterInterface[]
392
	 */
393 24
	public function getFilters($interface)
394
	{
395 24
		$filters = FilterFactory::create($this, $interface);
396 24
		foreach ($this->currentFilters as $filter)
397
		{
398 4
			if (!$filter instanceof $interface)
399 4
			{
400 2
				continue;
401
			}
402 4
			$filters[] = $filter;
403 24
		}
404 24
		return $filters;
405
	}
406
407
	/**
408
	 * Get logger
409
	 * @codeCoverageIgnore
410
	 * @return LoggerInterface
411
	 */
412
	public function getLogger()
413
	{
414
		return $this->loggerInstance;
415
	}
416
417
	/**
418
	 * Set logger
419
	 * @codeCoverageIgnore
420
	 * @param LoggerInterface $logger
421
	 * @return Signal
422
	 */
423
	public function setLogger(LoggerInterface $logger)
424
	{
425
		$this->loggerInstance = $logger;
426
		return $this;
427
	}
428
429
	/**
430
	 * Get dependency injection container
431
	 * @return EmbeDi
432
	 */
433 24
	public function getDi()
434
	{
435 24
		return $this->di;
436
	}
437
438
	public function setDi(EmbeDi $di)
439
	{
440
		$this->di = $di;
441
		return $this;
442
	}
443
444
	/**
445
	 * Get Input/Output adapter
446
	 * @codeCoverageIgnore
447
	 * @return BuilderIOInterface I/O Adapter
448
	 */
449
	public function getIO()
450
	{
451
		return $this->getConfigured('io');
452
	}
453
454
	/**
455
	 * Set Input/Output interface
456
	 * @codeCoverageIgnore
457
	 * @param BuilderIOInterface $io
458
	 * @return Signal
459
	 */
460
	public function setIO(BuilderIOInterface $io)
461
	{
462
		return $this->setConfigured($io, 'io');
463
	}
464
465
	/**
466
	 * @codeCoverageIgnore
467
	 * @return ExtractorInterface
468
	 */
469
	public function getExtractor()
470
	{
471
		return $this->getConfigured('extractor');
472
	}
473
474
	/**
475
	 * @codeCoverageIgnore
476
	 * @param ExtractorInterface $extractor
477
	 * @return Signal
478
	 */
479
	public function setExtractor(ExtractorInterface $extractor)
480
	{
481
		return $this->setConfigured($extractor, 'extractor');
482
	}
483
484
	/**
485
	 * Reloads signals cache and reinitializes component.
486
	 */
487
	public function resetCache()
488
	{
489
		$this->reload();
490
	}
491
492
	private function reload()
493
	{
494
		self::$config = $this->getIO()->read();
495
	}
496
497
	/**
498
	 * Get configured property
499
	 * @param string $property
500
	 * @return SignalAwareInterface
501
	 */
502 1
	private function getConfigured($property)
503
	{
504 1
		if (is_object($this->$property))
505 1
		{
506 1
			$object = $this->$property;
507 1
		}
508
		else
509
		{
510
			$object = $this->di->apply($this->$property);
511
		}
512 1
		if ($object instanceof SignalAwareInterface)
513 1
		{
514 1
			$object->setSignal($this);
515 1
		}
516 1
		return $object;
517
	}
518
519
	/**
520
	 * Set signal aware property
521
	 * @param SignalAwareInterface $object
522
	 * @param string $property
523
	 * @return Signal
524
	 */
525
	private function setConfigured(SignalAwareInterface $object, $property)
526
	{
527
		$object->setSignal($this);
528
		$this->$property = $object;
529
		return $this;
530
	}
531
532
}
533