Completed
Push — master ( c632d0...4a7120 )
by Peter
23:08
created

Addendum   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 10.29%

Importance

Changes 12
Bugs 1 Features 0
Metric Value
wmc 25
c 12
b 1
f 0
lcom 1
cbo 10
dl 0
loc 202
ccs 7
cts 68
cp 0.1029
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getData() 0 6 1
A getPaths() 0 4 1
A setSignal() 0 4 1
A log() 0 5 1
A __construct() 0 12 2
D processFile() 0 98 16
A hasSignals() 0 11 3
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 15
			self::SignalFor
81
		];
82 15
		foreach ($annotations as $annotation)
83
		{
84 15
			$annotation = preg_replace('~^@~', '', $annotation);
85 15
			$this->patterns[] = sprintf('~@%s~', $annotation);
86
		}
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();
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