Completed
Push — master ( 4a7120...86a668 )
by Peter
05:17
created

Signal   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 481
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 67.07%

Importance

Changes 44
Bugs 5 Features 6
Metric Value
wmc 57
c 44
b 5
f 6
lcom 1
cbo 12
dl 0
loc 481
rs 6.433
ccs 112
cts 167
cp 0.6707

21 Methods

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