Completed
Push — master ( 030596...ce4a72 )
by Peter
08:49
created

Signal::attach()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
ccs 0
cts 5
cp 0
cc 2
eloc 4
nc 2
nop 2
crap 6
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
 * @link https://maslosoft.com/signals/
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 is directory where config cache from yml file will
63
	 * be stored. Path is relative to project root. This must be writable
64
	 * by command line user.
65
	 *
66
	 * @var string
67
	 */
68
	public $runtimePath = 'runtime';
69
70
	/**
71
	 * This paths will be searched for `SlotFor` and `SignalFor` annotations.
72
	 *
73
	 *
74
	 *
75
	 * TODO Autodetect based on composer autoload
76
	 *
77
	 * @var string[]
78
	 */
79
	public $paths = [
80
		'vendor',
81
	];
82
83
	/**
84
	 * Directories to ignore while scanning
85
	 * @var string[]
86
	 */
87
	public $ignoreDirs = [
88
		'vendor', // Vendors in vendors
89
		'generated', // Generated data, including signals
90
		'runtime', // Runtime data
91
	];
92
93
	/**
94
	 * Filters configuration.
95
	 * This filters will be applied to every emit. This property
96
	 * should contain array of class names implementing filters.
97
	 * @var string[]|object[]
98
	 */
99
	public $filters = [];
100
101
	/**
102
	 * Sorters configuration.
103
	 * @var string[]|object[]
104
	 */
105
	public $sorters = [];
106
107
	/**
108
	 * Extractor configuration
109
	 * @var string|[]|object
110
	 */
111
	public $extractor = Addendum::class;
112
113
	/**
114
	 * Input/Output configuration, at minimum it should
115
	 * contain class name for builder input output interface.
116
	 * It can also contain array [configurable options for IO class](php-io/).
117
	 *
118
	 *
119
	 *
120
	 * @var string|[]|object
121
	 */
122
	public $io = PhpFile::class;
123
124
	/**
125
	 * Whenever component is initialized
126
	 * @var bool
127
	 */
128
	private $isInitialized = false;
129
130
	/**
131
	 * Configuration of signals and slots
132
	 * @var string[][]
133
	 */
134
	private static $config = [];
135
136
	/**
137
	 * Extra configurations of signals and slots
138
	 * @var array
139
	 */
140
	private static $configs = [];
141
142
	/**
143
	 * Logger instance holder
144
	 * NOTE: There is property annotation with `logger` name,
145
	 * thus this name is a bit longer
146
	 * @var LoggerInterface
147
	 */
148
	private $loggerInstance = null;
149
150
	/**
151
	 * Embedded dependency injection
152
	 * @var EmbeDi
153
	 */
154
	private $di = null;
155
156
	/**
157
	 * Version
158
	 * @var string
159
	 */
160
	private $version = null;
161
162
	/**
163
	 * Current filters
164
	 * @var PreFilterInterface[]|PostFilterInterface[]
165
	 */
166
	private $currentFilters = [];
167
168 25
	public function __construct($configName = self::ConfigName)
169
	{
170 25
		$this->loggerInstance = new NullLogger;
171
172
		/**
173
		 * TODO This should be made as embedi adapter, currently unsupported
174
		 */
175 25
		$config = new ConfigReader($configName);
176 25
		$this->di = EmbeDi::fly($configName);
177 25
		$this->di->configure($this);
178 25
		$this->di->apply($config->toArray(), $this);
179 25
	}
180
181
	/**
182
	 * Getter
183
	 * @param string $name
184
	 * @return mixed
185
	 */
186
	public function __get($name)
187
	{
188
		return $this->{'get' . ucfirst($name)}();
189
	}
190
191
	/**
192
	 * Setter
193
	 * @param string $name
194
	 * @param mixed $value
195
	 * @return mixed
196
	 */
197
	public function __set($name, $value)
198
	{
199
		return $this->{'set' . ucfirst($name)}($value);
200
	}
201
202
	/**
203
	 * Get current signals version
204
	 *
205
	 * @codeCoverageIgnore
206
	 * @return string
207
	 */
208
	public function getVersion()
209
	{
210
		if (null === $this->version)
211
		{
212
			$this->version = require __DIR__ . '/version.php';
213
		}
214
		return $this->version;
215
	}
216
217
	public function init()
218
	{
219
		if (!$this->isInitialized)
220
		{
221
			$this->reload();
222
		}
223
		if (!$this->di->isStored($this))
224
		{
225
			$this->di->store($this);
226
		}
227
	}
228
229
	/**
230
	 * Attach additional signals and slots configuration
231
	 * @param      $config
232
	 * @param bool $reload
233
	 */
234
	public function attach($config, $reload = true)
235
	{
236
		self::$configs[] = $config;
237
		if($reload)
238
		{
239
			$this->reload();
240
		}
241
	}
242
243
	/**
244
	 * Apply filter to current emit.
245
	 *
246
	 * Pass false as param to disable all filters.
247
	 *
248
	 * @param FilterInterface|string|mixed $filter
249
	 * @return Signal
250
	 * @throws UnexpectedValueException
251
	 */
252 4
	public function filter($filter)
253
	{
254
		// disable filters
255 4
		if (is_bool($filter) && false === $filter)
256
		{
257
			$this->currentFilters = [];
258
			return $this;
259
		}
260
		// Instantiate from string or array
261 4
		if (!is_object($filter))
262
		{
263 1
			$filter = $this->di->apply($filter);
264
		}
265 4
		if (!$filter instanceof PreFilterInterface && !$filter instanceof PostFilterInterface)
266
		{
267
			throw new UnexpectedValueException(sprintf('$filter must implement either `%s` or `%s` interface', PreFilterInterface::class, PostFilterInterface::class));
268
		}
269 4
		$this->currentFilters[] = $filter;
270 4
		return $this;
271
	}
272
273
	/**
274
	 * Emit signal to inform slots
275
	 * @param object|string $signal
276
	 * @return object[]
277
	 */
278 17
	public function emit($signal)
279
	{
280 17
		$result = [];
281 17
		if (is_string($signal))
282
		{
283
			$signal = new $signal;
284
		}
285 17
		$name = get_class($signal);
286 17
		NameNormalizer::normalize($name);
287 17
		if (empty(self::$config))
288
		{
289
			$this->init();
290
		}
291 17
		if (!isset(self::$config[self::Signals][$name]))
292
		{
293
			self::$config[self::Signals][$name] = [];
294
			$this->loggerInstance->debug('No slots found for signal `{name}`, skipping', ['name' => $name]);
295
			return $result;
296
		}
297
298 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...
299
		{
300
			// Skip
301 17
			if (false === $injections || count($injections) == 0)
302
			{
303
				continue;
304
			}
305 17
			if (!PreFilter::filter($this, $fqn, $signal))
306
			{
307 3
				continue;
308
			}
309 14
			foreach ($injections as $injection)
310
			{
311 14
				$injected = SlotFactory::create($this, $signal, $fqn, $injection);
312 14
				if (false === $injected)
313
				{
314
					continue;
315
				}
316 14
				if (!PostFilter::filter($this, $injected, $signal))
317
				{
318 2
					continue;
319
				}
320 14
				$result[] = $injected;
321
			}
322
		}
323 17
		$this->currentFilters = [];
324 17
		return $result;
325
	}
326
327
	/**
328
	 * Call for signals from slot
329
	 * @param object $slot
330
	 * @param string $interface Interface or class name which must be implemented, instanceof or sub class of to get into slot
331
	 */
332 7
	public function gather($slot, $interface = null)
333
	{
334 7
		$name = get_class($slot);
335 7
		NameNormalizer::normalize($name);
336 7
		if (!empty($interface))
337
		{
338 3
			NameNormalizer::normalize($interface);
339
		}
340 7
		if (empty(self::$config))
341
		{
342
			$this->init();
343
		}
344 7
		if (!isset(self::$config[self::Slots][$name]))
345
		{
346
			self::$config[self::Slots][$name] = [];
347
			$this->loggerInstance->debug('No signals found for slot `{name}`, skipping', ['name' => $name]);
348
		}
349 7
		$result = [];
350 7
		foreach ((array) self::$config[self::Slots][$name] as $fqn => $emit)
351
		{
352 7
			if (false === $emit)
353
			{
354
				continue;
355
			}
356 7
			if (!PreFilter::filter($this, $fqn, $slot))
357
			{
358
				continue;
359
			}
360
			// Check if class exists and log if doesn't
361 7
			if (!ClassChecker::exists($fqn))
362
			{
363
				$this->loggerInstance->debug(sprintf("Class `%s` not found while gathering slot `%s`", $fqn, get_class($slot)));
364
				continue;
365
			}
366 7
			if (null === $interface)
367
			{
368 4
				$injected = new $fqn;
369 4
				if (!PostFilter::filter($this, $injected, $slot))
370
				{
371 2
					continue;
372
				}
373 2
				$result[] = $injected;
374 2
				continue;
375
			}
376
377
			// Check if it's same as interface
378 3
			if ($fqn === $interface)
379
			{
380 1
				$injected = new $fqn;
381 1
				if (!PostFilter::filter($this, $injected, $slot))
382
				{
383
					continue;
384
				}
385 1
				$result[] = $injected;
386 1
				continue;
387
			}
388
389 3
			$info = new ReflectionClass($fqn);
390
391
			// Check if class is instance of base class
392 3
			if ($info->isSubclassOf($interface))
393
			{
394 2
				$injected = new $fqn;
395 2
				if (!PostFilter::filter($this, $injected, $slot))
396
				{
397
					continue;
398
				}
399 2
				$result[] = $injected;
400 2
				continue;
401
			}
402
403 3
			$interfaceInfo = new ReflectionClass($interface);
404
			// Check if class implements interface
405 3
			if ($interfaceInfo->isInterface() && $info->implementsInterface($interface))
406
			{
407
				$injected = new $fqn;
408
				if (!PostFilter::filter($this, $injected, $slot))
409
				{
410
					continue;
411
				}
412
				$result[] = $injected;
413 3
				continue;
414
			}
415
		}
416 7
		return $result;
417
	}
418
419
	/**
420
	 * Get filters
421
	 * @param string $interface
422
	 * @return PreFilterInterface[]|PostFilterInterface[]
423
	 */
424 24
	public function getFilters($interface)
425
	{
426 24
		$filters = FilterFactory::create($this, $interface);
427 24
		foreach ($this->currentFilters as $filter)
428
		{
429 4
			if (!$filter instanceof $interface)
430
			{
431 2
				continue;
432
			}
433 4
			$filters[] = $filter;
434
		}
435 24
		return $filters;
436
	}
437
438
	/**
439
	 * Get logger
440
	 * @codeCoverageIgnore
441
	 * @return LoggerInterface
442
	 */
443
	public function getLogger()
444
	{
445
		return $this->loggerInstance;
446
	}
447
448
	/**
449
	 * Set logger
450
	 * @codeCoverageIgnore
451
	 * @param LoggerInterface $logger
452
	 * @return Signal
453
	 */
454
	public function setLogger(LoggerInterface $logger)
455
	{
456
		$this->loggerInstance = $logger;
457
		return $this;
458
	}
459
460
	/**
461
	 * Get dependency injection container
462
	 * @return EmbeDi
463
	 */
464 24
	public function getDi()
465
	{
466 24
		return $this->di;
467
	}
468
469
	public function setDi(EmbeDi $di)
470
	{
471
		$this->di = $di;
472
		return $this;
473
	}
474
475
	/**
476
	 * Get Input/Output adapter
477
	 * @codeCoverageIgnore
478
	 * @return BuilderIOInterface I/O Adapter
479
	 */
480
	public function getIO()
481
	{
482
		return $this->getConfigured('io');
483
	}
484
485
	/**
486
	 * Set Input/Output interface
487
	 * @codeCoverageIgnore
488
	 * @param BuilderIOInterface $io
489
	 * @return Signal
490
	 */
491
	public function setIO(BuilderIOInterface $io)
492
	{
493
		return $this->setConfigured($io, 'io');
494
	}
495
496
	/**
497
	 * @codeCoverageIgnore
498
	 * @return ExtractorInterface
499
	 */
500
	public function getExtractor()
501
	{
502
		return $this->getConfigured('extractor');
503
	}
504
505
	/**
506
	 * @codeCoverageIgnore
507
	 * @param ExtractorInterface $extractor
508
	 * @return Signal
509
	 */
510
	public function setExtractor(ExtractorInterface $extractor)
511
	{
512
		return $this->setConfigured($extractor, 'extractor');
513
	}
514
515
	/**
516
	 * Reloads signals cache and reinitializes component.
517
	 */
518
	public function resetCache()
519
	{
520
		$this->reload();
521
	}
522
523
	private function reload()
524
	{
525
		self::$config = $this->getIO()->read();
526
527
		foreach(self::$configs as $config)
528
		{
529
			self::$config = array_replace_recursive(self::$config, $config);
530
		}
531
	}
532
533
	/**
534
	 * Get configured property
535
	 * @param string $property
536
	 * @return SignalAwareInterface
537
	 */
538 1
	private function getConfigured($property)
539
	{
540 1
		if (is_object($this->$property))
541
		{
542 1
			$object = $this->$property;
543
		}
544
		else
545
		{
546
			$object = $this->di->apply($this->$property);
547
		}
548 1
		if ($object instanceof SignalAwareInterface)
549
		{
550 1
			$object->setSignal($this);
551
		}
552 1
		return $object;
553
	}
554
555
	/**
556
	 * Set signal aware property
557
	 * @param SignalAwareInterface $object
558
	 * @param string $property
559
	 * @return Signal
560
	 */
561
	private function setConfigured(SignalAwareInterface $object, $property)
562
	{
563
		$object->setSignal($this);
564
		$this->$property = $object;
565
		return $this;
566
	}
567
568
}
569