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