Completed
Push — master ( 881261...525c77 )
by Peter
05:12
created

Signal::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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