Completed
Push — master ( 0a2e51...56c0fa )
by Peter
03:01
created

Addendum::autoloadHandler()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 4
nop 1
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\ParseException;
17
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
18
use Maslosoft\Addendum\Utilities\AnnotationUtility;
19
use Maslosoft\Addendum\Utilities\ClassChecker;
20
use Maslosoft\Addendum\Utilities\FileWalker;
21
use Maslosoft\Addendum\Utilities\NameNormalizer;
22
use Maslosoft\Signals\Exceptions\ClassNotFoundException;
23
use Maslosoft\Signals\Helpers\DataSorter;
24
use Maslosoft\Signals\Interfaces\ExtractorInterface;
25
use Maslosoft\Signals\Meta\DocumentMethodMeta;
26
use Maslosoft\Signals\Meta\DocumentPropertyMeta;
27
use Maslosoft\Signals\Meta\DocumentTypeMeta;
28
use Maslosoft\Signals\Meta\SignalsMeta;
29
use Maslosoft\Signals\Signal;
30
use ParseError;
31
use Psr\Log\LoggerInterface;
32
use ReflectionException;
33
use UnexpectedValueException;
34
35
/**
36
 * Addendum extractor
37
 * @codeCoverageIgnore
38
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
39
 */
40
class Addendum implements ExtractorInterface
41
{
42
43
	// Data keys for annotations extraction
44
	const SlotFor = 'SlotFor';
45
	const SignalFor = 'SignalFor';
46
	// Default annotation names
47
	const SlotName = 'SlotFor';
48
	const SignalName = 'SignalFor';
49
50
	/**
51
	 * Signal instance
52
	 * @var Signal
53
	 */
54
	private $signal = null;
55
56
	/**
57
	 * Signals and slots data
58
	 * @var mixed
59
	 */
60
	private $data = [
61
		Signal::Slots => [
62
		],
63
		Signal::Signals => [
64
		]
65
	];
66
67
	/**
68
	 * Scanned file paths
69
	 * @var string[]
70
	 */
71
	private $paths = [];
72
73
	/**
74
	 * Annotations matching patterns
75
	 * @var string[]
76
	 */
77
	private $patterns = [];
78
	private static $file = '';
79
80
	public function __construct()
81
	{
82
		$annotations = [
83
			self::SlotFor,
84
			self::SignalFor
85
		];
86
		foreach ($annotations as $annotation)
87
		{
88
			$annotation = preg_replace('~^@~', '', $annotation);
89
			$this->patterns[] = sprintf('~@%s~', $annotation);
90
		}
91
	}
92
93
	/**
94
	 * Handler for class not found errors
95
	 *
96
	 * @internal This must be public, but should not be used anywhere else
97
	 * @param string $className
98
	 */
99
	public static function autoloadHandler($className)
100
	{
101
		// These are loaded in some other way...
102
		if ($className === 'PHP_Invoker')
103
		{
104
			return false;
105
		}
106
		if (stripos($className, 'phpunit') !== false)
107
		{
108
			return false;
109
		}
110
		if (!ClassChecker::exists($className))
111
		{
112
			throw new ClassNotFoundException("Class $className not found when processing " . self::$file);
113
		}
114
		return false;
115
	}
116
117
	/**
118
	 * Get signals and slots data
119
	 * @return mixed
120
	 */
121
	public function getData()
122
	{
123
		(new FileWalker([], [$this, 'processFile'], $this->signal->paths, $this->signal->ignoreDirs))->walk();
124
		DataSorter::sort($this->data);
125
		return $this->data;
126
	}
127
128
	/**
129
	 * Get scanned paths. This is available only after getData call.
130
	 * @return string[]
131
	 */
132
	public function getPaths()
133
	{
134
		return $this->paths;
135
	}
136
137
	/**
138
	 * Set signal instance
139
	 * @param Signal $signal
140
	 */
141
	public function setSignal(Signal $signal)
142
	{
143
		$this->signal = $signal;
144
	}
145
146
	/**
147
	 * Get logger
148
	 * @return LoggerInterface
149
	 */
150
	public function getLogger()
151
	{
152
		return $this->signal->getLogger();
153
	}
154
155
	/**
156
	 * @param string $file
157
	 */
158
	public function processFile($file, $contents)
159
	{
160
		$this->getLogger()->debug("Processing `$file`");
161
		$file = realpath($file);
162
163
		self::$file = $file;
164
165
		$ignoreFile = sprintf('%s/.signalignore', dirname($file));
166
167
		if (file_exists($ignoreFile))
168
		{
169
			$this->getLogger()->notice("Skipping `$file` because of `$ignoreFile`" . PHP_EOL);
170
			return;
171
		}
172
173
		$this->paths[] = $file;
174
		// Remove initial `\` from namespace
175
		try
176
		{
177
			$annotated = AnnotationUtility::rawAnnotate($file);
178
		}
179
		catch (ClassNotFoundException $e)
180
		{
181
			$this->log($e, $file);
182
			return;
183
		}
184
		catch (ParseException $e)
185
		{
186
			$this->err($e, $file);
187
			return;
188
		}
189
		catch (UnexpectedValueException $e)
190
		{
191
			$this->err($e, $file);
192
			return;
193
		}
194
		catch (Exception $e)
195
		{
196
			$this->err($e, $file);
197
			return;
198
		}
199
200
		$namespace = preg_replace('~^\\\\+~', '', $annotated['namespace']);
201
		$className = $annotated['className'];
202
203
204
		// Use fully qualified name, class must autoload
205
		$fqn = $namespace . '\\' . $className;
206
		NameNormalizer::normalize($fqn);
207
208
		try
209
		{
210
			// NOTE: This autloader must be registered on ReflectionClass
211
			// creation ONLY! That's why register/unregister.
212
			// This will detect not found depending classes
213
			// (base classes,interfaces,traits etc.)
214
			$autoload = static::class . '::autoloadHandler';
215
			spl_autoload_register($autoload);
216
			eval('$info = new ReflectionClass($fqn);');
1 ignored issue
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
217
			spl_autoload_unregister($autoload);
218
		}
219
		catch (ParseError $e)
220
		{
221
			$this->err($e, $file);
222
			return;
223
		}
224
		catch (ClassNotFoundException $e)
225
		{
226
			$this->log($e, $file);
227
			return;
228
		}
229
		catch (ReflectionException $e)
230
		{
231
			$this->err($e, $file);
232
			return;
233
		}
234
		$isAnnotated = $info->implementsInterface(AnnotatedInterface::class);
1 ignored issue
show
Bug introduced by
The variable $info does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
235
		$hasSignals = $this->hasSignals($contents);
236
		$isAbstract = $info->isAbstract() || $info->isInterface();
237
238
		if ($isAnnotated)
239
		{
240
			$this->getLogger()->debug("Annotated: $info->name");
241
		}
242
		else
243
		{
244
			$this->getLogger()->debug("Not annotated: $info->name");
245
		}
246
247
		// Old classes must now implement interface
248
		// Brake BC!
249
		if ($hasSignals && !$isAnnotated && !$isAbstract)
250
		{
251
			throw new UnexpectedValueException(sprintf('Class %s must implement %s to use signals', $fqn, AnnotatedInterface::class));
252
		}
253
254
		// Skip not annotated class
255
		if (!$isAnnotated)
256
		{
257
			return;
258
		}
259
260
		// Skip abstract classes
261
		if ($isAbstract)
262
		{
263
			return;
264
		}
265
		try
266
		{
267
			// Discard notices (might be the case when outdated cache?)
268
			$level = error_reporting();
269
			error_reporting(E_WARNING);
270
			$meta = SignalsMeta::create($fqn);
271
			error_reporting($level);
272
		}
273
		catch (ParseException $e)
274
		{
275
			$this->err($e, $file);
276
			return;
277
		}
278
		catch (ClassNotFoundException $e)
279
		{
280
			$this->log($e, $file);
281
			return;
282
		}
283
		catch (UnexpectedValueException $e)
284
		{
285
			$this->err($e, $file);
286
			return;
287
		}
288
289
		/* @var $typeMeta DocumentTypeMeta */
290
		$typeMeta = $meta->type();
291
292
		// Signals
293
		foreach ($typeMeta->signalFor as $slot)
294
		{
295
			$this->getLogger()->debug("Signal: $slot:$fqn");
296
			$this->data[Signal::Slots][$slot][$fqn] = true;
297
		}
298
299
		// Slots
300
		// For constructor injection
301
		foreach ($typeMeta->slotFor as $slot)
302
		{
303
			$key = implode('::', [$fqn, '__construct']) . '()';
304
			$this->getLogger()->debug("Slot: $slot:$fqn$key");
305
			$this->data[Signal::Signals][$slot][$fqn][$key] = true;
306
		}
307
308
		// For method injection
309
		foreach ($meta->methods() as $methodName => $method)
310
		{
311
			/* @var $method DocumentMethodMeta */
312
			foreach ($method->slotFor as $slot)
313
			{
314
				$key = implode('::', [$fqn, $methodName]) . '()';
315
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
316
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s()', $methodName);
317
			}
318
		}
319
320
		// For property injection
321
		foreach ($meta->fields() as $fieldName => $field)
322
		{
323
			/* @var $field DocumentPropertyMeta */
324
			foreach ($field->slotFor as $slot)
325
			{
326
				$key = implode('::$', [$fqn, $fieldName]);
327
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
328
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s', $fieldName);
329
			}
330
		}
331
	}
332
333
	private function hasSignals($contents)
334
	{
335
		foreach ($this->patterns as $pattern)
336
		{
337
			if (preg_match($pattern, $contents))
338
			{
339
				return true;
340
			}
341
		}
342
		return false;
343
	}
344
345
	private function log(Exception $e, $file)
346
	{
347
		$msg = sprintf('Warning: %s while scanning file `%s`', $e->getMessage(), $file);
348
		$msg = $msg . PHP_EOL;
349
		$this->signal->getLogger()->warning($msg);
350
	}
351
352
	private function err(Exception $e, $file)
353
	{
354
		$msg = sprintf('Error: %s while scanning file `%s`', $e->getMessage(), $file);
355
		$msg = $msg . PHP_EOL;
356
		$this->signal->getLogger()->error($msg);
357
	}
358
359
}
360