These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * phpDocumentor |
||
4 | * |
||
5 | * PHP Version 5.3 |
||
6 | * |
||
7 | * @copyright 2010-2014 Mike van Riel / Naenius (http://www.naenius.com) |
||
8 | * @license http://www.opensource.org/licenses/mit-license.php MIT |
||
9 | * @link http://phpdoc.org |
||
10 | */ |
||
11 | |||
12 | namespace phpDocumentor\Compiler\Linker; |
||
13 | |||
14 | use phpDocumentor\Compiler\CompilerPassInterface; |
||
15 | use phpDocumentor\Descriptor\ClassDescriptor; |
||
16 | use phpDocumentor\Descriptor\Collection; |
||
17 | use phpDocumentor\Descriptor\DescriptorAbstract; |
||
18 | use phpDocumentor\Descriptor\FileDescriptor; |
||
19 | use phpDocumentor\Descriptor\InterfaceDescriptor; |
||
20 | use phpDocumentor\Descriptor\Interfaces\ProjectInterface; |
||
21 | use phpDocumentor\Descriptor\NamespaceDescriptor; |
||
22 | use phpDocumentor\Descriptor\ProjectDescriptor; |
||
23 | use phpDocumentor\Descriptor\TraitDescriptor; |
||
24 | use phpDocumentor\Descriptor\Type\UnknownTypeDescriptor; |
||
25 | |||
26 | /** |
||
27 | * The linker contains all rules to replace FQSENs in the ProjectDescriptor with aliases to objects. |
||
28 | * |
||
29 | * This object contains a list of class FQCNs for Descriptors and their associated linker rules. |
||
30 | * |
||
31 | * An example scenario should be: |
||
32 | * |
||
33 | * The Descriptor ``\phpDocumentor\Descriptor\ClassDescriptor`` has a *Substitute* rule determining that the |
||
34 | * contents of the ``Parent`` field should be substituted with another ClassDescriptor with the FQCN |
||
35 | * represented by the value of the Parent field. In addition (second element) it has an *Analyse* rule |
||
36 | * specifying that the contents of the ``Methods`` field should be interpreted by the linker. Because that field |
||
37 | * contains an array or Descriptor Collection will each element be analysed by the linker. |
||
38 | * |
||
39 | * As can be seen in the above example is it possible to analyse a tree structure and substitute FQSENs where |
||
40 | * encountered. |
||
41 | */ |
||
42 | class Linker implements CompilerPassInterface |
||
43 | { |
||
44 | const COMPILER_PRIORITY = 10000; |
||
45 | |||
46 | const CONTEXT_MARKER = '@context'; |
||
47 | |||
48 | /** @var DescriptorAbstract[] */ |
||
49 | protected $elementList = array(); |
||
50 | |||
51 | /** @var string[][] */ |
||
52 | protected $substitutions = array(); |
||
53 | |||
54 | /** @var string[] Prevent cycles by tracking which objects have been analyzed */ |
||
55 | protected $processedObjects = array(); |
||
56 | |||
57 | /** |
||
58 | * {@inheritDoc} |
||
59 | */ |
||
60 | public function getDescription() |
||
61 | { |
||
62 | return 'Replace textual FQCNs with object aliases'; |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * Initializes the linker with a series of Descriptors to link to. |
||
67 | * |
||
68 | * @param array|string[][] $substitutions |
||
69 | */ |
||
70 | public function __construct(array $substitutions) |
||
71 | { |
||
72 | $this->substitutions = $substitutions; |
||
73 | } |
||
74 | |||
75 | /** |
||
76 | * Executes the linker. |
||
77 | * |
||
78 | * @param ProjectDescriptor $project Representation of the Object Graph that can be manipulated. |
||
79 | * |
||
80 | * @return void |
||
81 | */ |
||
82 | public function execute(ProjectDescriptor $project) |
||
83 | { |
||
84 | $this->setObjectAliasesList($project->getIndexes()->elements->getAll()); |
||
85 | $this->substitute($project); |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * Returns the list of substitutions for the linker. |
||
90 | * |
||
91 | * @return string[] |
||
92 | */ |
||
93 | public function getSubstitutions() |
||
94 | { |
||
95 | return $this->substitutions; |
||
96 | } |
||
97 | |||
98 | /** |
||
99 | * Sets the list of object aliases to resolve the FQSENs with. |
||
100 | * |
||
101 | * @param DescriptorAbstract[] $elementList |
||
102 | * |
||
103 | * @return void |
||
104 | */ |
||
105 | public function setObjectAliasesList(array $elementList) |
||
106 | { |
||
107 | $this->elementList = $elementList; |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * Substitutes the given item or its children's FQCN with an object alias. |
||
112 | * |
||
113 | * This method may do either of the following depending on the item's type |
||
114 | * |
||
115 | * String |
||
116 | * If the given item is a string then this method will attempt to find an appropriate Class, Interface or |
||
117 | * TraitDescriptor object and return that. See {@see findAlias()} for more information on the normalization |
||
118 | * of these strings. |
||
119 | * |
||
120 | * Array or Traversable |
||
121 | * Iterate through each item, pass each key's contents to a new call to substitute and replace the key's |
||
122 | * contents if the contents is not an object (objects automatically update and saves performance). |
||
123 | * |
||
124 | * Object |
||
125 | * Determines all eligible substitutions using the substitutions property, construct a getter and retrieve |
||
126 | * the field's contents. Pass these contents to a new call of substitute and use a setter to replace the field's |
||
127 | * contents if anything other than null is returned. |
||
128 | * |
||
129 | * This method will return null if no substitution was possible and all of the above should not update the parent |
||
130 | * item when null is passed. |
||
131 | * |
||
132 | * @param string|object|\Traversable|array $item |
||
133 | * @param DescriptorAbstract|null $container A descriptor that acts as container for all elements |
||
134 | * underneath or null if there is no current container. |
||
135 | * |
||
136 | * @return null|string|DescriptorAbstract |
||
137 | */ |
||
138 | public function substitute($item, $container = null) |
||
139 | { |
||
140 | $result = null; |
||
141 | |||
142 | if (is_string($item)) { |
||
143 | $result = $this->findAlias($item, $container); |
||
144 | } elseif (is_array($item) || ($item instanceof \Traversable && ! $item instanceof ProjectInterface)) { |
||
145 | $isModified = false; |
||
146 | foreach ($item as $key => $element) { |
||
147 | $isModified = true; |
||
148 | |||
149 | $element = $this->substitute($element, $container); |
||
150 | if ($element !== null) { |
||
151 | $item[$key] = $element; |
||
152 | } |
||
153 | } |
||
154 | if ($isModified) { |
||
155 | $result = $item; |
||
156 | } |
||
157 | } elseif (is_object($item) && $item instanceof UnknownTypeDescriptor) { |
||
158 | $alias = $this->findAlias($item->getName()); |
||
159 | $result = $alias ?: $item; |
||
160 | } elseif (is_object($item)) { |
||
161 | $hash = spl_object_hash($item); |
||
162 | if (isset($this->processedObjects[$hash])) { |
||
163 | // if analyzed; just return |
||
164 | return null; |
||
165 | } |
||
166 | |||
167 | $newContainer = ($this->isDescriptorContainer($item)) ? $item : $container; |
||
168 | |||
169 | $this->processedObjects[$hash] = true; |
||
170 | |||
171 | $objectClassName = get_class($item); |
||
172 | $fieldNames = isset($this->substitutions[$objectClassName]) |
||
173 | ? $this->substitutions[$objectClassName] |
||
174 | : array(); |
||
175 | |||
176 | foreach ($fieldNames as $fieldName) { |
||
177 | $fieldValue = $this->findFieldValue($item, $fieldName); |
||
178 | $response = $this->substitute($fieldValue, $newContainer); |
||
0 ignored issues
–
show
|
|||
179 | |||
180 | // if the returned response is not an object it must be grafted on the calling object |
||
181 | if ($response !== null) { |
||
182 | $setter = 'set'.ucfirst($fieldName); |
||
183 | $item->$setter($response); |
||
184 | } |
||
185 | } |
||
186 | } |
||
187 | |||
188 | return $result; |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * Attempts to find a Descriptor object alias with the FQSEN of the element it represents. |
||
193 | * |
||
194 | * This method will try to fetch an element after normalizing the provided FQSEN. The FQSEN may contain references |
||
195 | * (bindings) that can only be resolved during linking (such as `self`) or it may contain a context marker |
||
196 | * {@see CONTEXT_MARKER}. |
||
197 | * |
||
198 | * If there is a context marker then this method will see if a child of the given container exists that matches the |
||
199 | * element following the marker. If such a child does not exist in the current container then the namespace is |
||
200 | * queried if a child exists there that matches. |
||
201 | * |
||
202 | * For example: |
||
203 | * |
||
204 | * Given the Fqsen `@context::myFunction()` and the lastContainer `\My\Class` will this method first check |
||
205 | * to see if `\My\Class::myFunction()` exists; if it doesn't it will then check if `\My\myFunction()` exists. |
||
206 | * |
||
207 | * If neither element exists then this method assumes it is an undocumented class/trait/interface and change the |
||
208 | * given FQSEN by returning the namespaced element name (thus in the example above that would be |
||
209 | * `\My\myFunction()`). The calling method {@see substitute()} will then replace the value of the field containing |
||
210 | * the context marker with this normalized string. |
||
211 | * |
||
212 | * @param string $fqsen |
||
213 | * @param DescriptorAbstract|null $container |
||
214 | * |
||
215 | * @return DescriptorAbstract|string|null |
||
216 | */ |
||
217 | public function findAlias($fqsen, $container = null) |
||
218 | { |
||
219 | $fqsen = $this->replacePseudoTypes($fqsen, $container); |
||
220 | |||
221 | if ($this->isContextMarkerInFqsen($fqsen) && $container instanceof DescriptorAbstract) { |
||
222 | // first exchange `@context::element` for `\My\Class::element` and if it exists, return that |
||
223 | $classMember = $this->fetchElementByFqsen($this->getTypeWithClassAsContext($fqsen, $container)); |
||
224 | if ($classMember) { |
||
225 | return $classMember; |
||
226 | } |
||
227 | |||
228 | // otherwise exchange `@context::element` for `\My\element` and if it exists, return that |
||
229 | $namespaceContext = $this->getTypeWithNamespaceAsContext($fqsen, $container); |
||
230 | $namespaceMember = $this->fetchElementByFqsen($namespaceContext); |
||
0 ignored issues
–
show
Are you sure the assignment to
$namespaceMember is correct as $this->fetchElementByFqsen($namespaceContext) (which targets phpDocumentor\Compiler\L...::fetchElementByFqsen() ) seems to always return null.
This check looks for function or method calls that always return null and whose return value is assigned to a variable. class A
{
function getObject()
{
return null;
}
}
$a = new A();
$object = $a->getObject();
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.
Loading history...
|
|||
231 | if ($namespaceMember) { |
||
232 | return $namespaceMember; |
||
233 | } |
||
234 | |||
235 | // otherwise check if the element exists in the global namespace and if it exists, return that |
||
236 | $globalNamespaceContext = $this->getTypeWithGlobalNamespaceAsContext($fqsen); |
||
237 | $globalNamespaceMember = $this->fetchElementByFqsen($globalNamespaceContext); |
||
0 ignored issues
–
show
Are you sure the assignment to
$globalNamespaceMember is correct as $this->fetchElementByFqs...globalNamespaceContext) (which targets phpDocumentor\Compiler\L...::fetchElementByFqsen() ) seems to always return null.
This check looks for function or method calls that always return null and whose return value is assigned to a variable. class A
{
function getObject()
{
return null;
}
}
$a = new A();
$object = $a->getObject();
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.
Loading history...
|
|||
238 | if ($globalNamespaceMember) { |
||
239 | return $globalNamespaceMember; |
||
240 | } |
||
241 | |||
242 | // Otherwise we assume it is an undocumented class/interface/trait and return `\My\element` so |
||
243 | // that the name containing the marker may be replaced by the class reference as string |
||
244 | return $namespaceContext; |
||
245 | } |
||
246 | |||
247 | return $this->fetchElementByFqsen($fqsen); |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * Returns the value of a field in the given object. |
||
252 | * |
||
253 | * @param object $object |
||
254 | * @param string $fieldName |
||
255 | * |
||
256 | * @return string|object |
||
257 | */ |
||
258 | public function findFieldValue($object, $fieldName) |
||
259 | { |
||
260 | $getter = 'get'.ucfirst($fieldName); |
||
261 | |||
262 | return $object->$getter(); |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * Returns true if the given Descriptor is a container type. |
||
267 | * |
||
268 | * @param DescriptorAbstract|mixed $item |
||
269 | * |
||
270 | * @return bool |
||
271 | */ |
||
272 | protected function isDescriptorContainer($item) |
||
273 | { |
||
274 | return $item instanceof FileDescriptor |
||
275 | || $item instanceof NamespaceDescriptor |
||
276 | || $item instanceof ClassDescriptor |
||
277 | || $item instanceof TraitDescriptor |
||
278 | || $item instanceof InterfaceDescriptor; |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * Replaces pseudo-types, such as `self`, into a normalized version based on the last container that was |
||
283 | * encountered. |
||
284 | * |
||
285 | * @param string $fqsen |
||
286 | * @param DescriptorAbstract|null $container |
||
287 | * |
||
288 | * @return string |
||
289 | */ |
||
290 | protected function replacePseudoTypes($fqsen, $container) |
||
291 | { |
||
292 | $pseudoTypes = array('self', '$this'); |
||
293 | foreach ($pseudoTypes as $pseudoType) { |
||
294 | if ((strpos($fqsen, $pseudoType . '::') === 0 || $fqsen === $pseudoType) && $container) { |
||
295 | $fqsen = $container->getFullyQualifiedStructuralElementName() |
||
296 | . substr($fqsen, strlen($pseudoType)); |
||
297 | } |
||
298 | } |
||
299 | |||
300 | return $fqsen; |
||
301 | } |
||
302 | |||
303 | /** |
||
304 | * Returns true if the context marker is found in the given FQSEN. |
||
305 | * |
||
306 | * @param string $fqsen |
||
307 | * |
||
308 | * @return bool |
||
309 | */ |
||
310 | protected function isContextMarkerInFqsen($fqsen) |
||
311 | { |
||
312 | return strpos($fqsen, self::CONTEXT_MARKER) !== false; |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
317 | * |
||
318 | * @param string $fqsen |
||
319 | * @param DescriptorAbstract $container |
||
320 | * |
||
321 | * @return string |
||
322 | */ |
||
323 | protected function getTypeWithClassAsContext($fqsen, DescriptorAbstract $container) |
||
324 | { |
||
325 | if (!$container instanceof ClassDescriptor |
||
326 | && !$container instanceof InterfaceDescriptor |
||
327 | && !$container instanceof TraitDescriptor |
||
328 | ) { |
||
329 | return $fqsen; |
||
330 | } |
||
331 | |||
332 | $containerFqsen = $container->getFullyQualifiedStructuralElementName(); |
||
333 | |||
334 | return str_replace(self::CONTEXT_MARKER . '::', $containerFqsen . '::', $fqsen); |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
339 | * |
||
340 | * @param string $fqsen |
||
341 | * @param DescriptorAbstract $container |
||
342 | * |
||
343 | * @return string |
||
344 | */ |
||
345 | protected function getTypeWithNamespaceAsContext($fqsen, DescriptorAbstract $container) |
||
346 | { |
||
347 | $namespace = $container instanceof NamespaceDescriptor ? $container : $container->getNamespace(); |
||
348 | $fqnn = $namespace instanceof NamespaceDescriptor |
||
349 | ? $namespace->getFullyQualifiedStructuralElementName() |
||
350 | : $namespace; |
||
351 | |||
352 | return str_replace(self::CONTEXT_MARKER . '::', $fqnn . '\\', $fqsen); |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Normalizes the given FQSEN as if the context marker represents the global namespace as parent. |
||
357 | * |
||
358 | * @param string $fqsen |
||
359 | * @return string |
||
360 | */ |
||
361 | protected function getTypeWithGlobalNamespaceAsContext($fqsen) |
||
362 | { |
||
363 | return str_replace(self::CONTEXT_MARKER . '::', '\\', $fqsen); |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * Attempts to find an element with the given Fqsen in the list of elements for this project and returns null if |
||
368 | * it cannot find it. |
||
369 | * |
||
370 | * @param string $fqsen |
||
371 | * |
||
372 | * @return DescriptorAbstract|null |
||
373 | */ |
||
374 | protected function fetchElementByFqsen($fqsen) |
||
375 | { |
||
376 | return isset($this->elementList[$fqsen]) ? $this->elementList[$fqsen] : null; |
||
377 | } |
||
378 | } |
||
379 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.