Signal   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 526
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 60.29%

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 12
dl 0
loc 526
rs 3.44
c 0
b 0
f 0
ccs 82
cts 136
cp 0.6029

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A __get() 0 4 1
A __set() 0 4 1
A getVersion() 0 8 2
A init() 0 11 3
A attach() 0 8 2
B filter() 0 20 6
B emit() 0 48 11
D gather() 0 87 17
A getFilters() 0 13 3
A getLogger() 0 4 1
A setLogger() 0 5 1
A getDi() 0 4 1
A setDi() 0 5 1
A getIO() 0 4 1
A setIO() 0 4 1
A getExtractor() 0 4 1
A setExtractor() 0 4 1
A resetCache() 0 4 1
A reload() 0 9 2
A getConfigured() 0 16 3
A setConfigured() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Signal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Signal, and based on these observations, apply Extract Interface, too.

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
531
		}
532
	}
533
534
	/**
535
	 * Get configured property
536
	 * @param string $property
537
	 * @return SignalAwareInterface|ExtractorInterface|BuilderIOInterface
538
	 */
539 1
	private function getConfigured($property)
540
	{
541 1
		if (is_object($this->$property))
542
		{
543 1
			$object = $this->$property;
544
		}
545
		else
546
		{
547
			$object = $this->di->apply($this->$property);
548
		}
549 1
		if ($object instanceof SignalAwareInterface)
550
		{
551 1
			$object->setSignal($this);
552
		}
553 1
		return $object;
554
	}
555
556
	/**
557
	 * Set signal aware property
558
	 * @param SignalAwareInterface $object
559
	 * @param string $property
560
	 * @return Signal
561
	 */
562
	private function setConfigured(SignalAwareInterface $object, $property)
563
	{
564
		$object->setSignal($this);
565
		$this->$property = $object;
566
		return $this;
567
	}
568
569
}
570