Completed
Push — master ( 4c2930...4201b5 )
by Peter
05:41
created

Builder::normalizeFqn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL, Commercial license.
5
 *
6
 * @package maslosoft/addendum
7
 * @licence AGPL, Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]> (Meta container, further improvements, bugfixes)
9
 * @copyright Copyright (c) Maslosoft (Meta container, further improvements, bugfixes)
10
 * @copyright Copyright (c) Jan Suchal (Original version, builder, parser)
11
 * @link http://maslosoft.com/addendum/ - maslosoft addendum
12
 * @link https://code.google.com/p/addendum/ - original addendum project
13
 */
14
15
namespace Maslosoft\Addendum\Builder;
16
17
use Exception;
18
use Maslosoft\Addendum\Addendum;
19
use Maslosoft\Addendum\Annotations\TargetAnnotation;
20
use Maslosoft\Addendum\Cache\BuildOneCache;
21
use Maslosoft\Addendum\Collections\AnnotationsCollection;
22
use Maslosoft\Addendum\Collections\MatcherConfig;
23
use Maslosoft\Addendum\Helpers\CoarseChecker;
24
use Maslosoft\Addendum\Interfaces\AnnotationInterface;
25
use Maslosoft\Addendum\Matcher\AnnotationsMatcher;
26
use Maslosoft\Addendum\Reflection\ReflectionAnnotatedClass;
27
use Maslosoft\Addendum\Reflection\ReflectionAnnotatedMethod;
28
use Maslosoft\Addendum\Reflection\ReflectionAnnotatedProperty;
29
use Maslosoft\Addendum\Utilities\Blacklister;
30
use Maslosoft\Addendum\Utilities\ClassChecker;
31
use Maslosoft\Addendum\Utilities\NameNormalizer;
32
use Maslosoft\Addendum\Utilities\ReflectionName;
33
use ReflectionClass;
34
use ReflectionMethod;
35
use ReflectionProperty;
36
use Reflector;
37
38
/**
39
 * @Label("Annotations builder")
40
 */
41
class Builder
42
{
43
44
	/**
45
	 * Cached values of parsing
46
	 * @var string[][][]
47
	 */
48
	private static $cache = [];
49
50
	/**
51
	 * Addendum instance
52
	 * @var Addendum
53
	 */
54
	private $addendum = null;
55
56
	/**
57
	 * One entity build cache
58
	 * @var BuildOneCache
59
	 */
60
	private $buildCache = null;
61
62 50
	public function __construct(Addendum $addendum = null)
63
	{
64 50
		$this->addendum = $addendum ?: new Addendum();
65 50
		$this->buildCache = new BuildOneCache(static::class, null, $this->addendum);
66 50
	}
67
68
	/**
69
	 * Build annotations collection
70
	 * @param ReflectionAnnotatedClass|ReflectionAnnotatedMethod|ReflectionAnnotatedProperty $targetReflection
71
	 * @return AnnotationsCollection
72
	 */
73 50
	public function build($targetReflection)
74
	{
75 50
		$annotations = [];
76
77 50
		$data = $this->buildOne($targetReflection);
78
79
		// Get annotations from current entity
80 50
		foreach ($data as $class => $parameters)
81
		{
82 48
			foreach ($parameters as $params)
83
			{
84 48
				$annotation = $this->instantiateAnnotation($class, $params, $targetReflection);
85 48
				if ($annotation !== false)
86
				{
87 48
					$annotations[$class][] = $annotation;
88
				}
89
			}
90
		}
91
92 50
		return new AnnotationsCollection($annotations);
93
	}
94
95 50
	private function buildOne($targetReflection)
96
	{
97 50
		if (empty($targetReflection))
98
		{
99 22
			return [];
100
		}
101
		// Decide where from take traits and base classes.
102
		// Either from class if it's being processed reflection class
103
		// or from declaring class if it's being processed for properties and
104
		// methods
105 50
		if ($targetReflection instanceof ReflectionClass)
106
		{
107 50
			$targetClass = $targetReflection;
108
		}
109
		else
110
		{
111 29
			$targetClass = $targetReflection->getDeclaringClass();
112
		}
113
114
		// Check if currently reflected component is cached
115 50
		$this->buildCache->setComponent(ReflectionName::createName($targetReflection));
116 50
		$cached = $this->buildCache->get();
117
118
		// Check if currently reflected component *class* is cached
119 50
		$this->buildCache->setComponent(ReflectionName::createName($targetClass));
120 50
		$cachedClass = $this->buildCache->get();
121
122
		// Cache is valid only if class cache is valid,
123
		// this is required for Conflicts and Target checks
124 50
		if (false !== $cached && false !== $cachedClass)
125
		{
126 50
			return $cached;
127
		}
128
129
		// Parials annotation data
130 46
		$partialsData = [];
131
132
		// Extract annotations from all partials
133 46
		foreach ($this->getPartials($targetClass) as $partial)
134
		{
135
			// Need to recurse here, as:
136
			//
137
			// * Interface might extend from other interfaces
138
			// * Parent class, might have traits or interfaces
139
			// * Trait might have traits
140 44
			$partData = $this->buildOne($this->getRelated($targetReflection, $partial));
141
142
			// Check if data is present and proper
143 44
			if (false === $partData || empty($partData) || !is_array($partData))
144
			{
145 39
				continue;
146
			}
147
148 17
			$partialsData = array_merge_recursive($partialsData, $partData);
149
		}
150
151
		// Merge data from traits etc.
152
		// with data from class
153 46
		$data = array_merge_recursive($partialsData, $this->parse($targetReflection));
154
155 46
		$this->buildCache->setComponent(ReflectionName::createName($targetReflection));
156 46
		$this->buildCache->set($data);
157 46
		return $data;
158
	}
159
160
	/**
161
	 * Get reflection for related target reflection.
162
	 * Will return class, property or method reflection from related reflection,
163
	 * based on type of target reflection.
164
	 *
165
	 * Will return false target reflection id for method or property, and
166
	 * method or property does not exists on related reflection.
167
	 *
168
	 * @param Reflector $targetReflection
169
	 * @param ReflectionClass $relatedReflection
170
	 * @return ReflectionClass|ReflectionProperty|ReflectionMethod|bool
171
	 */
172 44
	private function getRelated(Reflector $targetReflection, ReflectionClass $relatedReflection)
173
	{
174
		switch (true)
175
		{
176 44
			case $targetReflection instanceof ReflectionClass:
177 36
				return $relatedReflection;
178 20
			case $targetReflection instanceof ReflectionProperty && $relatedReflection->hasProperty($targetReflection->name):
179 9
				return $relatedReflection->getProperty($targetReflection->name);
180 9
			case $targetReflection instanceof ReflectionMethod && $relatedReflection->hasMethod($targetReflection->name):
181 6
				return $relatedReflection->getMethod($targetReflection->name);
182
		}
183 22
		return false;
184
	}
185
186
	/**
187
	 * Get partials of class, this includes:
188
	 *
189
	 * * Parent class
190
	 * * Interfaces
191
	 * * Traits
192
	 *
193
	 * @param ReflectionClass $targetClass
194
	 * @return ReflectionAnnotatedClass[]
195
	 */
196 46
	private function getPartials(ReflectionClass $targetClass)
197
	{
198
		// Partial reflections
199
		/* @var ReflectionAnnotatedClass[] */
200 46
		$partials = [];
201
202
		// Collect current class interfaces
203 46
		foreach ($targetClass->getInterfaces() as $targetInterface)
204
		{
205
			/* @var $targetInterface ReflectionAnnotatedClass */
206 41
			$partials[] = $targetInterface;
207
		}
208
209
		// Collect current class parent class
210 46
		$targetParent = $targetClass->getParentClass();
211 46
		if (!empty($targetParent))
212
		{
213 25
			$partials[] = $targetParent;
214
		}
215
216
		// Collect current class traits
217 46
		foreach ($targetClass->getTraits() as $trait)
218
		{
219
			/* @var $trait ReflectionAnnotatedClass */
220 10
			$partials[] = $trait;
221
		}
222 46
		return $partials;
223
	}
224
225
	/**
226
	 * Create new instance of annotation
227
	 * @param string $class
228
	 * @param mixed[] $parameters
229
	 * @param ReflectionAnnotatedClass|ReflectionAnnotatedMethod|ReflectionAnnotatedProperty|bool $targetReflection
230
	 * @return boolean|object
231
	 */
232 48
	public function instantiateAnnotation($class, $parameters, $targetReflection = false)
233
	{
234 48
		$class = ucfirst($class) . "Annotation";
235
236
		// If namespaces are empty assume global namespace
237 48
		$fqn = $this->normalizeFqn('\\', $class);
238 48
		$mandatoryNs = TargetAnnotation::Ns;
239 48
		$namespaces = $this->addendum->namespaces;
240 48
		if (!in_array($mandatoryNs, $namespaces))
241
		{
242
			$namespaces[] = $mandatoryNs;
243
		}
244 48
		foreach ($namespaces as $ns)
245
		{
246 48
			$fqn = $this->normalizeFqn($ns, $class);
247 48
			if (Blacklister::ignores($fqn))
248
			{
249 37
				continue;
250
			}
251
			try
252
			{
253 48
				if (!ClassChecker::exists($fqn))
254
				{
255 45
					$this->addendum->getLogger()->debug('Annotation class `{fqn}` not found, ignoring', ['fqn' => $fqn]);
256 45
					Blacklister::ignore($fqn);
257
				}
258
				else
259
				{
260
					// Class exists, exit loop
261 48
					break;
262
				}
263
			}
264
			catch (Exception $e)
265
			{
266
				// Ignore class autoloading errors
267
			}
268
		}
269 48
		if (Blacklister::ignores($fqn))
270
		{
271 1
			return false;
272
		}
273
		try
274
		{
275
			// NOTE: was class_exists here, however should be safe to use ClassChecker::exists here
276 48
			if (!ClassChecker::exists($fqn))
277
			{
278
				$this->addendum->getLogger()->debug('Annotation class `{fqn}` not found, ignoring', ['fqn' => $fqn]);
279
				Blacklister::ignore($fqn);
280
				return false;
281
			}
282
		}
283
		catch (Exception $e)
284
		{
285
			// Ignore autoload errors and return false
286
			Blacklister::ignore($fqn);
287
			return false;
288
		}
289 48
		$resolvedClass = Addendum::resolveClassName($fqn);
290 48
		if ((new ReflectionClass($resolvedClass))->implementsInterface(AnnotationInterface::class) || $resolvedClass == AnnotationInterface::class)
291
		{
292 48
			return new $resolvedClass($parameters, $targetReflection);
293
		}
294
		return false;
295
	}
296
297
	/**
298
	 * Normalize class name and namespace to proper fully qualified name
299
	 * @param string $ns
300
	 * @param string $class
301
	 * @return string
302
	 */
303 48
	private function normalizeFqn($ns, $class)
304
	{
305 48
		$fqn = "\\$ns\\$class";
306 48
		NameNormalizer::normalize($fqn);
307 48
		return $fqn;
308
	}
309
310
	/**
311
	 * Get doc comment
312
	 * @param ReflectionAnnotatedClass|ReflectionAnnotatedMethod|ReflectionAnnotatedProperty $reflection
313
	 * @return mixed[]
314
	 */
315 46
	private function parse($reflection)
316
	{
317 46
		$key = sprintf('%s@%s', $this->addendum->getInstanceId(), ReflectionName::createName($reflection));
318 46
		if (!isset(self::$cache[$key]))
319
		{
320
			//
321 46
			if (!CoarseChecker::mightHaveAnnotations($reflection))
322
			{
323 26
				self::$cache[$key] = [];
324 26
				return self::$cache[$key];
325
			}
326 43
			$parser = new AnnotationsMatcher;
327 43
			$data = [];
328 43
			$parser->setPlugins(new MatcherConfig([
329 43
				'addendum' => $this->addendum,
330 43
				'reflection' => $reflection
331
			]));
332 43
			$parser->matches($this->getDocComment($reflection), $data);
333 43
			self::$cache[$key] = $data;
334
		}
335 43
		return self::$cache[$key];
336
	}
337
338
	/**
339
	 * Get doc comment
340
	 * @param ReflectionAnnotatedClass|ReflectionAnnotatedMethod|ReflectionAnnotatedProperty $reflection
341
	 * @return mixed[]
342
	 */
343 43
	protected function getDocComment($reflection)
344
	{
345 43
		return Addendum::getDocComment($reflection);
346
	}
347
348
	/**
349
	 * Clear local parsing cache
350
	 */
351 3
	public static function clearCache()
352
	{
353 3
		self::$cache = [];
354 3
	}
355
356
}
357