Completed
Push — master ( 900abc...5e4fcd )
by Peter
07:01
created

Addendum::debug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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