Completed
Push — master ( e560bc...809be1 )
by Peter
05:23
created

Addendum::getLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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\FileWalker;
20
use Maslosoft\Addendum\Utilities\NameNormalizer;
21
use Maslosoft\Signals\Helpers\DataSorter;
22
use Maslosoft\Signals\Interfaces\ExtractorInterface;
23
use Maslosoft\Signals\Meta\DocumentMethodMeta;
24
use Maslosoft\Signals\Meta\DocumentPropertyMeta;
25
use Maslosoft\Signals\Meta\DocumentTypeMeta;
26
use Maslosoft\Signals\Meta\SignalsMeta;
27
use Maslosoft\Signals\Signal;
28
use Psr\Log\LoggerInterface;
29
use ReflectionClass;
30
use ReflectionException;
31
use UnexpectedValueException;
32
33
/**
34
 * Addendum extractor
35
 * @codeCoverageIgnore
36
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
37
 */
38
class Addendum implements ExtractorInterface
39
{
40
41
	// Data keys for annotations extraction
42
	const SlotFor = 'SlotFor';
43
	const SignalFor = 'SignalFor';
44
	// Default annotation names
45
	const SlotName = 'SlotFor';
46
	const SignalName = 'SignalFor';
47
48
	/**
49
	 * Signal instance
50
	 * @var Signal
51
	 */
52
	private $signal = null;
53
54
	/**
55
	 * Signals and slots data
56
	 * @var mixed
57
	 */
58
	private $data = [
59
		Signal::Slots => [
60
		],
61
		Signal::Signals => [
62
		]
63
	];
64
65
	/**
66
	 * Scanned file paths
67
	 * @var string[]
68
	 */
69
	private $paths = [];
70
71
	/**
72
	 * Annotations matching patterns
73
	 * @var string[]
74
	 */
75
	private $patterns = [];
76
77
	public function __construct()
78
	{
79
		$annotations = [
80
			self::SlotFor,
81
			self::SignalFor
82
		];
83
		foreach ($annotations as $annotation)
84
		{
85
			$annotation = preg_replace('~^@~', '', $annotation);
86
			$this->patterns[] = sprintf('~@%s~', $annotation);
87
		}
88
	}
89
90
	/**
91
	 * Get signals and slots data
92
	 * @return mixed
93
	 */
94
	public function getData()
95
	{
96
		(new FileWalker([], [$this, 'processFile'], $this->signal->paths, $this->signal->ignoreDirs))->walk();
97
		DataSorter::sort($this->data);
98
		return $this->data;
99
	}
100
101
	/**
102
	 * Get scanned paths. This is available only after getData call.
103
	 * @return string[]
104
	 */
105
	public function getPaths()
106
	{
107
		return $this->paths;
108
	}
109
110
	/**
111
	 * Set signal instance
112
	 * @param Signal $signal
113
	 */
114
	public function setSignal(Signal $signal)
115
	{
116
		$this->signal = $signal;
117
	}
118
119
	/**
120
	 * Get logger
121
	 * @return LoggerInterface
122
	 */
123
	public function getLogger()
124
	{
125
		return $this->signal->getLogger();
126
	}
127
128
	/**
129
	 * @param string $file
130
	 */
131
	public function processFile($file, $contents)
132
	{
133
		$this->getLogger()->debug("Processing `$file`");
134
		$file = realpath($file);
135
136
		$ignoreFile = sprintf('%s/.signalignore', dirname($file));
137
138
		if (file_exists($ignoreFile))
139
		{
140
			$this->getLogger()->notice("Skipping `$file` because of `$ignoreFile`" . PHP_EOL);
141
			return;
142
		}
143
144
		$this->paths[] = $file;
145
		// Remove initial `\` from namespace
146
		try
147
		{
148
			$annotated = AnnotationUtility::rawAnnotate($file);
149
		}
150
		catch (ParseException $e)
151
		{
152
			$this->err($e, $file);
153
			return;
154
		}
155
		catch (UnexpectedValueException $e)
156
		{
157
			$this->log($e, $file);
158
			return;
159
		}
160
		$namespace = preg_replace('~^\\\\+~', '', $annotated['namespace']);
161
		$className = $annotated['className'];
162
163
164
		// Use fully qualified name, class must autoload
165
		$fqn = $namespace . '\\' . $className;
166
		NameNormalizer::normalize($fqn);
167
168
		try
169
		{
170
			$info = new ReflectionClass($fqn);
171
		}
172
		catch (ReflectionException $e)
173
		{
174
			$this->getLogger()->debug("Could not autoload $fqn");
175
			return;
176
		}
177
		$isAnnotated = $info->implementsInterface(AnnotatedInterface::class);
178
		$hasSignals = $this->hasSignals($contents);
179
		$isAbstract = $info->isAbstract() || $info->isInterface();
180
181
		if ($isAnnotated)
182
		{
183
			$this->getLogger()->debug("Annotated: $info->name");
184
		}
185
		else
186
		{
187
			$this->getLogger()->debug("Not annotated: $info->name");
188
		}
189
190
		// Old classes must now implement interface
191
		// Brake BC!
192
		if ($hasSignals && !$isAnnotated && !$isAbstract)
193
		{
194
			throw new UnexpectedValueException(sprintf('Class %s must implement %s to use signals', $fqn, AnnotatedInterface::class));
195
		}
196
197
		// Skip not annotated class
198
		if (!$isAnnotated)
199
		{
200
			return;
201
		}
202
203
		// Skip abstract classes
204
		if ($isAbstract)
205
		{
206
			return;
207
		}
208
		try
209
		{
210
			// Discard notices (might be the case when outdated cache?)
211
			$level = error_reporting();
212
			error_reporting(E_WARNING);
213
			$meta = SignalsMeta::create($fqn);
214
			error_reporting($level);
215
		}
216
		catch (ParseException $e)
217
		{
218
			$this->err($e, $file);
219
			return;
220
		}
221
		catch (UnexpectedValueException $e)
222
		{
223
			$this->err($e, $file);
224
			return;
225
		}
226
227
		/* @var $typeMeta DocumentTypeMeta */
228
		$typeMeta = $meta->type();
229
230
		// Signals
231
		foreach ($typeMeta->signalFor as $slot)
232
		{
233
			$this->getLogger()->debug("Signal: $slot:$fqn");
234
			$this->data[Signal::Slots][$slot][$fqn] = true;
235
		}
236
237
		// Slots
238
		// For constructor injection
239
		foreach ($typeMeta->slotFor as $slot)
240
		{
241
			$key = implode('::', [$fqn, '__construct']) . '()';
242
			$this->getLogger()->debug("Slot: $slot:$fqn$key");
243
			$this->data[Signal::Signals][$slot][$fqn][$key] = true;
244
		}
245
246
		// For method injection
247
		foreach ($meta->methods() as $methodName => $method)
248
		{
249
			/* @var $method DocumentMethodMeta */
250
			foreach ($method->slotFor as $slot)
251
			{
252
				$key = implode('::', [$fqn, $methodName]) . '()';
253
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
254
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s()', $methodName);
255
			}
256
		}
257
258
		// For property injection
259
		foreach ($meta->fields() as $fieldName => $field)
260
		{
261
			/* @var $field DocumentPropertyMeta */
262
			foreach ($field->slotFor as $slot)
263
			{
264
				$key = implode('::$', [$fqn, $fieldName]);
265
				$this->getLogger()->debug("Slot: $slot:$fqn$key");
266
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s', $fieldName);
267
			}
268
		}
269
	}
270
271
	private function hasSignals($contents)
272
	{
273
		foreach ($this->patterns as $pattern)
274
		{
275
			if (preg_match($pattern, $contents))
276
			{
277
				return true;
278
			}
279
		}
280
		return false;
281
	}
282
283
	private function log(Exception $e, $file)
284
	{
285
		$msg = sprintf('Warning: %s while scanning file `%s`', $e->getMessage(), $file);
286
		$msg = $msg . PHP_EOL;
287
		$this->signal->getLogger()->warning($msg);
288
	}
289
290
	private function err(Exception $e, $file)
291
	{
292
		$msg = sprintf('Error: %s while scanning file `%s`', $e->getMessage(), $file);
293
		$msg = $msg . PHP_EOL;
294
		$this->signal->getLogger()->error($msg);
295
	}
296
297
}
298