Completed
Push — master ( b8d9fd...98dccd )
by Peter
06:07
created

src/Builder/Addendum.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
 *
11
 */
12
13
namespace Maslosoft\Signals\Builder;
14
15
use Maslosoft\Addendum\Exceptions\ParseException;
16
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
17
use Maslosoft\Addendum\Utilities\AnnotationUtility;
18
use Maslosoft\Addendum\Utilities\FileWalker;
19
use Maslosoft\Signals\Helpers\DataSorter;
20
use Maslosoft\Signals\Helpers\NameNormalizer;
21
use Maslosoft\Signals\Interfaces\ExtractorInterface;
22
use Maslosoft\Signals\Meta\DocumentMethodMeta;
23
use Maslosoft\Signals\Meta\DocumentPropertyMeta;
24
use Maslosoft\Signals\Meta\DocumentTypeMeta;
25
use Maslosoft\Signals\Meta\SignalsMeta;
26
use Maslosoft\Signals\Signal;
27
use ReflectionClass;
28
use ReflectionException;
29
use UnexpectedValueException;
30
use Exception;
31
32
/**
33
 * Addendum extractor
34
 *
35
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
36
 */
37
class Addendum implements ExtractorInterface
38
{
39
40
	// Data keys for annotations extraction
41
	const SlotFor = 'SlotFor';
42
	const SignalFor = 'SignalFor';
43
	// Default annotation names
44
	const SlotName = 'SlotFor';
45
	const SignalName = 'SignalFor';
46
47
	/**
48
	 * Signal instance
49
	 * @var Signal
50
	 */
51
	private $signal = null;
52
53
	/**
54
	 * Signals and slots data
55
	 * @var mixed
56
	 */
57
	private $data = [
58
		Signal::Slots => [
59
		],
60
		Signal::Signals => [
61
		]
62
	];
63
64
	/**
65
	 * Scanned file paths
66
	 * @var string[]
67
	 */
68
	private $paths = [];
69
70
	/**
71
	 * Annotations mathing patterns
72
	 * @var string[]
73
	 */
74
	private $patterns = [];
75
76 15
	public function __construct()
77
	{
78
		$annotations = [
79 15
			self::SlotFor,
80
			self::SignalFor
81 15
		];
82 15
		foreach ($annotations as $annotation)
83
		{
84 15
			$annotation = preg_replace('~^@~', '', $annotation);
85 15
			$this->patterns[] = sprintf('~@%s~', $annotation);
86 15
		}
87 15
	}
88
89
	/**
90
	 * Get signals and slots data
91
	 * @return mixed
92
	 */
93
	public function getData()
94
	{
95
		(new FileWalker([], [$this, 'processFile'], $this->signal->paths, $this->signal->ignoreDirs))->walk();
0 ignored issues
show
The call to FileWalker::__construct() has too many arguments starting with $this->signal->ignoreDirs.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
96
		DataSorter::sort($this->data);
97
		return $this->data;
98
	}
99
100
	/**
101
	 * Get scanned paths. This is available only after getData call.
102
	 * @return string[]
103
	 */
104
	public function getPaths()
105
	{
106
		return $this->paths;
107
	}
108
109
	/**
110
	 * Set signal instance
111
	 * @param Signal $signal
112
	 */
113
	public function setSignal(Signal $signal)
114
	{
115
		$this->signal = $signal;
116
	}
117
118
	/**
119
	 * @param string $file
120
	 */
121
	public function processFile($file, $contents)
122
	{
123
		$file = realpath($file);
124
		$this->paths[] = $file;
125
		// Remove initial `\` from namespace
126
		try
127
		{
128
			$annotated = AnnotationUtility::rawAnnotate($file);
129
		}
130
		catch (ParseException $e)
131
		{
132
			$this->log($e, $file);
133
			return;
134
		}
135
		catch (UnexpectedValueException $e)
136
		{
137
			$this->log($e, $file);
138
			return;
139
		}
140
		$namespace = preg_replace('~^\\\\+~', '', $annotated['namespace']);
141
		$className = $annotated['className'];
142
143
144
		// Use fully qualified name, class must autoload
145
		$fqn = $namespace . '\\' . $className;
146
		NameNormalizer::normalize($fqn);
147
148
		try
149
		{
150
			$info = new ReflectionClass($fqn);
151
		}
152
		catch (ReflectionException $e)
153
		{
154
			$this->log($e, $file);
155
			return;
156
		}
157
		$isAnnotated = $info->implementsInterface(AnnotatedInterface::class);
158
		$hasSignals = $this->hasSignals($contents);
159
		$isAbstract = $info->isAbstract() || $info->isInterface();
160
161
		// Old classes must now implement interface
162
		// Brake BC!
163
		if ($hasSignals && !$isAnnotated && !$isAbstract)
164
		{
165
			throw new UnexpectedValueException(sprintf('Class %s must implement %s to use signals', $fqn, AnnotatedInterface::class));
166
		}
167
168
		// Skip not annotated class
169
		if (!$isAnnotated)
170
		{
171
			return;
172
		}
173
174
		// Skip abstract classes
175
		if ($isAbstract)
176
		{
177
			return;
178
		}
179
		$meta = @SignalsMeta::create($fqn);
180
		/* @var $typeMeta DocumentTypeMeta */
181
		$typeMeta = $meta->type();
182
183
		// Signals
184
		foreach ($typeMeta->signalFor as $slot)
185
		{
186
			$this->data[Signal::Slots][$slot][$fqn] = true;
187
		}
188
189
		// Slots
190
		// For constructor injection
191
		foreach ($typeMeta->slotFor as $slot)
192
		{
193
			$key = implode('@', [$fqn, '__construct', '()']);
194
			$this->data[Signal::Signals][$slot][$fqn][$key] = true;
195
		}
196
197
		// For method injection
198
		foreach ($meta->methods() as $methodName => $method)
199
		{
200
			/* @var $method DocumentMethodMeta */
201
			foreach ($method->slotFor as $slot)
202
			{
203
				$key = implode('@', [$fqn, $methodName, '()']);
204
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s()', $methodName);
205
			}
206
		}
207
208
		// For property injection
209
		foreach ($meta->fields() as $fieldName => $field)
210
		{
211
			/* @var $field DocumentPropertyMeta */
212
			foreach ($field->slotFor as $slot)
213
			{
214
				$key = implode('@', [$fqn, $fieldName]);
215
				$this->data[Signal::Signals][$slot][$fqn][$key] = sprintf('%s', $fieldName);
216
			}
217
		}
218
	}
219
220
	private function hasSignals($contents)
221
	{
222
		foreach ($this->patterns as $pattern)
223
		{
224
			if (preg_match($pattern, $contents))
225
			{
226
				return true;
227
			}
228
		}
229
		return false;
230
	}
231
232
	private function log(Exception $e, $file)
233
	{
234
		$msg = sprintf('Exception: "%s" while scanning file `%s`', $e->getMessage(), $file);
235
		$this->signal->getLogger()->warning($msg);
236
	}
237
238
}
239