Completed
Push — master ( 997566...32009b )
by Peter
03:46
created

Builder::getDataFor()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.0084

Importance

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