Completed
Push — master ( 78b2a4...f78031 )
by Peter
13:24
created

Signal::gather()   D

Complexity

Conditions 17
Paths 104

Size

Total Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 24.6402

Importance

Changes 0
Metric Value
dl 0
loc 87
ccs 33
cts 47
cp 0.702
rs 4.4303
c 0
b 0
f 0
cc 17
nc 104
nop 2
crap 24.6402

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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