Addendum::processFile()   F
last analyzed

Complexity

Conditions 30
Paths 648

Size

Total Lines 197

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 197
rs 0.3911
c 0
b 0
f 0
cc 30
nc 648
nop 2

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 /** @noinspection PhpUndefinedClassInspection */
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\Builder;
14
15
use function class_implements;
16
use Exception;
17
use function implode;
18
use function is_a;
19
use Maslosoft\Addendum\Exceptions\NoClassInFileException;
20
use Maslosoft\Addendum\Exceptions\ParseException;
21
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
22
use Maslosoft\Addendum\Utilities\AnnotationUtility;
23
use Maslosoft\Addendum\Utilities\ClassChecker;
24
use Maslosoft\Addendum\Utilities\FileWalker;
25
use Maslosoft\Addendum\Utilities\NameNormalizer;
26
use Maslosoft\Signals\Exceptions\ClassNotFoundException;
27
use Maslosoft\Signals\Helpers\DataSorter;
28
use Maslosoft\Signals\Interfaces\ExtractorInterface;
29
use Maslosoft\Signals\Interfaces\PathsAwareInterface;
30
use Maslosoft\Signals\ISignal;
31
use Maslosoft\Signals\ISignalSlot;
32
use Maslosoft\Signals\Meta\DocumentMethodMeta;
33
use Maslosoft\Signals\Meta\DocumentPropertyMeta;
34
use Maslosoft\Signals\Meta\DocumentTypeMeta;
35
use Maslosoft\Signals\Meta\SignalsMeta;
36
use Maslosoft\Signals\Signal;
37
use ParseError;
38
use Psr\Log\LoggerInterface;
39
use ReflectionClass;
40
use ReflectionException;
41
use function sprintf;
42
use UnexpectedValueException;
43
44
/**
45
 * Addendum extractor
46
 * @codeCoverageIgnore
47
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
48
 */
49
class Addendum implements ExtractorInterface, PathsAwareInterface
50
{
51
52
	// Data keys for annotations extraction
53
	const SlotFor = 'SlotFor';
54
	const SignalFor = 'SignalFor';
55
	// Default annotation names
56
	const SlotName = 'SlotFor';
57
	const SignalName = 'SignalFor';
58
59
	/**
60
	 * Signal instance
61
	 * @var Signal
62
	 */
63
	private $signal = null;
64
65
	/**
66
	 * Signals and slots data
67
	 * @var mixed
68
	 */
69
	private $data = [
70
		Signal::Slots => [
71
		],
72
		Signal::Signals => [
73
		]
74
	];
75
76
	/**
77
	 * Scanned file paths
78
	 * @var string[]
79
	 */
80
	private $paths = [];
81
82
	/**
83
	 * Annotations matching patterns
84
	 * @var string[]
85
	 */
86
	private $patterns = [];
87
	private static $file = '';
88
89
	public function __construct()
90
	{
91
		$annotations = [
92
			self::SlotFor,
93
			self::SignalFor
94
		];
95
		foreach ($annotations as $annotation)
96
		{
97
			$annotation = preg_replace('~^@~', '', $annotation);
98
			$this->patterns[] = sprintf('~@%s~', $annotation);
99
		}
100
	}
101
102
	/**
103
	 * Handler for class not found errors
104
	 *
105
	 * @internal This must be public, but should not be used anywhere else
106
	 * @param string $className
107
	 * @return bool
108
	 */
109
	public static function autoloadHandler($className)
110
	{
111
		// These are loaded in some other way...
112
		if ($className === 'PHP_Invoker')
113
		{
114
			return false;
115
		}
116
		if ($className === '\PHP_Invoker')
117
		{
118
			return false;
119
		}
120
		if (stripos($className, 'phpunit') !== false)
121
		{
122
			return false;
123
		}
124
		if (!ClassChecker::exists($className))
125
		{
126
			throw new ClassNotFoundException("Class $className not found when processing " . self::$file);
127
		}
128
		return false;
129
	}
130
131
	/**
132
	 * Get signals and slots data
133
	 * @return mixed
134
	 */
135
	public function getData(): array
136
	{
137
		$paths = [];
138
		foreach($this->signal->paths as $path)
139
		{
140
			$real = realpath($path);
141
			if(false === $real)
142
			{
143
				$this->signal->getLogger()->warning("Directory $path could not be resolved to absolute path");
144
				continue;
145
			}
146
			$paths[] = $real;
147
		}
148
		(new FileWalker([], [$this, 'processFile'], $paths, $this->signal->ignoreDirs))->walk();
149
		DataSorter::sort($this->data);
150
		return $this->data;
151
	}
152
153
	/**
154
	 * Get scanned paths. This is available only after getData call.
155
	 * @return string[]
156
	 */
157
	public function getPaths():array
158
	{
159
		return $this->paths;
160
	}
161
162
	/**
163
	 * Set signal instance
164
	 * @param Signal $signal
165
	 */
166
	public function setSignal(Signal $signal)
167
	{
168
		$this->signal = $signal;
169
	}
170
171
	/**
172
	 * Get logger
173
	 * @return LoggerInterface
174
	 */
175
	public function getLogger()
176
	{
177
		return $this->signal->getLogger();
178
	}
179
180
	/**
181
	 * @param string $file
182
	 * @param        $contents
183
	 */
184
	public function processFile(string $file, string $contents)
185
	{
186
		$this->getLogger()->debug("Processing `$file`");
187
		$file = realpath($file);
188
189
		self::$file = $file;
190
191
		$ignoreFile = sprintf('%s/.signalignore', dirname($file));
192
193
		if (file_exists($ignoreFile))
194
		{
195
			$this->getLogger()->notice("Skipping `$file` because of `$ignoreFile`" . PHP_EOL);
196
			return;
197
		}
198
199
		$this->paths[] = $file;
200
		// Remove initial `\` from namespace
201
		try
202
		{
203
			$annotated = AnnotationUtility::rawAnnotate($file);
204
		}
205
		catch(NoClassInFileException $e)
206
		{
207
			$this->log($e, $file);
208
			return;
209
		}
210
		catch (ClassNotFoundException $e)
211
		{
212
			$this->log($e, $file);
213
			return;
214
		}
215
		catch (ParseException $e)
216
		{
217
			$this->err($e, $file);
218
			return;
219
		}
220
		catch (UnexpectedValueException $e)
221
		{
222
			$this->err($e, $file);
223
			return;
224
		}
225
		catch (Exception $e)
226
		{
227
			$this->err($e, $file);
228
			return;
229
		}
230
231
		$namespace = preg_replace('~^\\\\+~', '', $annotated['namespace']);
232
		$className = $annotated['className'];
233
234
235
		// Use fully qualified name, class must autoload
236
		$fqn = $namespace . '\\' . $className;
237
		NameNormalizer::normalize($fqn);
238
239
		try
240
		{
241
			// NOTE: This autoloader must be registered on ReflectionClass
242
			// creation ONLY! That's why register/unregister.
243
			// This will detect not found depending classes
244
			// (base classes,interfaces,traits etc.)
245
			$autoload = static::class . '::autoloadHandler';
246
			spl_autoload_register($autoload);
247
			eval('$info = new ReflectionClass($fqn);');
248
			spl_autoload_unregister($autoload);
249
		}
250
		catch (ParseError $e)
251
		{
252
			$this->err($e, $file);
253
			return;
254
		}
255
		catch (ClassNotFoundException $e)
256
		{
257
			$this->log($e, $file);
258
			return;
259
		}
260
		catch (ReflectionException $e)
261
		{
262
			$this->err($e, $file);
263
			return;
264
		}
265
		// $info is created in `eval`
266
		/* @var $info ReflectionClass */
267
		$isAnnotated = $info->implementsInterface(AnnotatedInterface::class);
268
		$hasSignals = $this->hasSignals($contents);
269
		$isAbstract = $info->isAbstract() || $info->isInterface();
270
271
		if ($isAnnotated)
272
		{
273
			$this->getLogger()->debug("Annotated: $info->name");
274
		}
275
		else
276
		{
277
			$this->getLogger()->debug("Not annotated: $info->name");
278
		}
279
280
		// Old classes must now implement interface
281
		// Brake BC!
282
		if ($hasSignals && !$isAnnotated && !$isAbstract)
283
		{
284
			throw new UnexpectedValueException(sprintf('Class %s must implement %s to use signals', $fqn, AnnotatedInterface::class));
285
		}
286
287
		// Skip not annotated class
288
		if (!$isAnnotated)
289
		{
290
			return;
291
		}
292
293
		// Skip abstract classes
294
		if ($isAbstract)
295
		{
296
			return;
297
		}
298
		try
299
		{
300
			// Discard notices (might be the case when outdated cache?)
301
			$level = error_reporting();
302
			error_reporting(E_WARNING);
303
			$meta = SignalsMeta::create($fqn);
304
			error_reporting($level);
305
		}
306
		catch (ParseException $e)
307
		{
308
			$this->err($e, $file);
309
			return;
310
		}
311
		catch (ClassNotFoundException $e)
312
		{
313
			$this->log($e, $file);
314
			return;
315
		}
316
		catch (UnexpectedValueException $e)
317
		{
318
			$this->err($e, $file);
319
			return;
320
		}
321
322
		/* @var $typeMeta DocumentTypeMeta */
323
		$typeMeta = $meta->type();
324
325
		// Signals
326
		foreach ($typeMeta->signalFor as $slot)
327
		{
328
			if(is_a($slot, ISignal::class, true))
329
			{
330
				$this->getLogger()->error(sprintf('Slot `%s` is used as signal on `%s`', $slot, $fqn));
331
			}
332
			$this->getLogger()->debug("Signal: $slot:$fqn");
333
			$this->data[Signal::Slots][$slot][$fqn] = true;
334
		}
335
336
		// Slots
337
		// For constructor injection
338
		foreach ($typeMeta->slotFor as $slot)
339
		{
340
			if(is_a($slot, ISignalSlot::class, true))
341
			{
342
				$this->getLogger()->error(sprintf('Signal `%s` is used as slot on `%s`', $slot, $fqn));
343
			}
344
			$key = implode('::', [$fqn, '__construct']) . '()';
345
			$this->getLogger()->debug("Slot: $slot:$fqn$key");
346
			$this->data[Signal::Signals][$slot][$fqn][$key] = true;
347
		}
348
349
		// For method injection
350
		foreach ($meta->methods() as $methodName => $method)
351
		{
352
			/* @var $method DocumentMethodMeta */
353
			foreach ($method->slotFor as $slot)
354
			{
355
				$key = implode('::', [$fqn, $methodName]) . '()';
356
				if(is_a($slot, ISignalSlot::class, true))
357
				{
358
					$this->getLogger()->error(sprintf('Signal `%s` is used as slot on `%s`', $slot, $key));
359
				}
360
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
361
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s()', $methodName);
362
			}
363
		}
364
365
		// For property injection
366
		foreach ($meta->fields() as $fieldName => $field)
367
		{
368
			/* @var $field DocumentPropertyMeta */
369
			foreach ($field->slotFor as $slot)
370
			{
371
				$key = implode('::$', [$fqn, $fieldName]);
372
				if(is_a($slot, ISignalSlot::class, true))
373
				{
374
					$this->getLogger()->error(sprintf('Signal `%s` is used as slot on `%s`', $slot, $key));
375
				}
376
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
377
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s', $fieldName);
378
			}
379
		}
380
	}
381
382
	private function hasSignals($contents)
383
	{
384
		foreach ($this->patterns as $pattern)
385
		{
386
			if (preg_match($pattern, $contents))
387
			{
388
				return true;
389
			}
390
		}
391
		return false;
392
	}
393
394
	private function log($e, $file)
395
	{
396
		/* @var $e ParseError|Exception */
397
		$msg = sprintf('Warning: %s while scanning file `%s`', $e->getMessage(), $file);
398
		$msg = $msg . PHP_EOL;
399
		$this->signal->getLogger()->warning($msg);
400
	}
401
402
	private function err($e, $file)
403
	{
404
		// Uncomment for debugging
405
		/* @var $e ParseError|Exception */
406
		$msg = sprintf('Error: %s while scanning file `%s`', $e->getMessage(), $file);
407
		$msg = $msg . PHP_EOL;
408
409
		// Don't output errors on Maslosoft test models
410
		if(preg_match('~Maslosoft\\\\\w+Test~', $e->getMessage()))
411
		{
412
			$this->signal->getLogger()->debug($msg);
413
			return;
414
		}
415
416
		// Skip PHP_Invoker class, as it is possibly deprecated anyway
417
		if(strstr($e->getMessage(), 'PHP_Invoker'))
418
		{
419
			$this->signal->getLogger()->debug($msg);
420
			return;
421
		}
422
		$this->signal->getLogger()->error($msg);
423
	}
424
425
}
426