Completed
Push — master ( 93c108...b5b8be )
by Peter
07:59
created

Builder::build()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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