Completed
Push — master ( b5b8be...5ff268 )
by Peter
13:31
created

Builder::getPartials()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 28
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

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