Completed
Push — master ( 6f7dec...421df6 )
by Peter
04:39
created

Addendum::err()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 34
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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