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-2018 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 = []; |
||
50 | |||
51 | /** @var string[][] */ |
||
52 | protected $substitutions = []; |
||
53 | |||
54 | /** @var string[] Prevent cycles by tracking which objects have been analyzed */ |
||
55 | protected $processedObjects = []; |
||
56 | |||
57 | /** |
||
58 | * {@inheritDoc} |
||
59 | */ |
||
60 | 1 | public function getDescription() |
|
61 | { |
||
62 | 1 | 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 | 4 | public function __construct(array $substitutions) |
|
71 | { |
||
72 | 4 | $this->substitutions = $substitutions; |
|
73 | 4 | } |
|
74 | |||
75 | /** |
||
76 | * Executes the linker. |
||
77 | * |
||
78 | * @param ProjectDescriptor $project Representation of the Object Graph that can be manipulated. |
||
79 | */ |
||
80 | 1 | public function execute(ProjectDescriptor $project) |
|
81 | { |
||
82 | 1 | $this->setObjectAliasesList($project->getIndexes()->elements->getAll()); |
|
0 ignored issues
–
show
|
|||
83 | 1 | $this->substitute($project); |
|
84 | 1 | } |
|
85 | |||
86 | /** |
||
87 | * Returns the list of substitutions for the linker. |
||
88 | * |
||
89 | * @return string[] |
||
90 | */ |
||
91 | 1 | public function getSubstitutions() |
|
92 | { |
||
93 | 1 | return $this->substitutions; |
|
94 | } |
||
95 | |||
96 | /** |
||
97 | * Sets the list of object aliases to resolve the FQSENs with. |
||
98 | * |
||
99 | * @param DescriptorAbstract[] $elementList |
||
100 | */ |
||
101 | 3 | public function setObjectAliasesList(array $elementList) |
|
102 | { |
||
103 | 3 | $this->elementList = $elementList; |
|
104 | 3 | } |
|
105 | |||
106 | /** |
||
107 | * Substitutes the given item or its children's FQCN with an object alias. |
||
108 | * |
||
109 | * This method may do either of the following depending on the item's type |
||
110 | * |
||
111 | * String |
||
112 | * If the given item is a string then this method will attempt to find an appropriate Class, Interface or |
||
113 | * TraitDescriptor object and return that. See {@see findAlias()} for more information on the normalization |
||
114 | * of these strings. |
||
115 | * |
||
116 | * Array or Traversable |
||
117 | * Iterate through each item, pass each key's contents to a new call to substitute and replace the key's |
||
118 | * contents if the contents is not an object (objects automatically update and saves performance). |
||
119 | * |
||
120 | * Object |
||
121 | * Determines all eligible substitutions using the substitutions property, construct a getter and retrieve |
||
122 | * the field's contents. Pass these contents to a new call of substitute and use a setter to replace the field's |
||
123 | * contents if anything other than null is returned. |
||
124 | * |
||
125 | * This method will return null if no substitution was possible and all of the above should not update the parent |
||
126 | * item when null is passed. |
||
127 | * |
||
128 | * @param string|object|\Traversable|array $item |
||
129 | * @param DescriptorAbstract|null $container A descriptor that acts as container for all elements |
||
130 | * underneath or null if there is no current container. |
||
131 | * |
||
132 | * @return null|string|DescriptorAbstract |
||
133 | */ |
||
134 | 7 | public function substitute($item, $container = null) |
|
135 | { |
||
136 | 7 | $result = null; |
|
137 | |||
138 | 7 | if (is_string($item)) { |
|
139 | 5 | $result = $this->findAlias($item, $container); |
|
140 | 7 | } elseif (is_array($item) || ($item instanceof \Traversable && ! $item instanceof ProjectInterface)) { |
|
141 | 2 | $isModified = false; |
|
142 | 2 | foreach ($item as $key => $element) { |
|
143 | 2 | $isModified = true; |
|
144 | |||
145 | 2 | $element = $this->substitute($element, $container); |
|
146 | 2 | if ($element !== null) { |
|
147 | 2 | $item[$key] = $element; |
|
148 | } |
||
149 | } |
||
150 | |||
151 | 2 | if ($isModified) { |
|
152 | 2 | $result = $item; |
|
153 | } |
||
154 | 6 | } elseif (is_object($item) && $item instanceof UnknownTypeDescriptor) { |
|
155 | 1 | $alias = $this->findAlias($item->getName()); |
|
156 | 1 | $result = $alias ?: $item; |
|
157 | 5 | } elseif (is_object($item)) { |
|
158 | 5 | $hash = spl_object_hash($item); |
|
159 | 5 | if (isset($this->processedObjects[$hash])) { |
|
160 | // if analyzed; just return |
||
161 | 1 | return null; |
|
162 | } |
||
163 | |||
164 | 5 | $newContainer = ($this->isDescriptorContainer($item)) ? $item : $container; |
|
165 | |||
166 | 5 | $this->processedObjects[$hash] = true; |
|
167 | |||
168 | 5 | $objectClassName = get_class($item); |
|
169 | 5 | $fieldNames = $this->substitutions[$objectClassName] ?? []; |
|
170 | |||
171 | 5 | foreach ($fieldNames as $fieldName) { |
|
172 | 4 | $fieldValue = $this->findFieldValue($item, $fieldName); |
|
173 | 4 | $response = $this->substitute($fieldValue, $newContainer); |
|
0 ignored issues
–
show
It seems like
$newContainer defined by $this->isDescriptorConta...m) ? $item : $container on line 164 can also be of type object ; however, phpDocumentor\Compiler\Linker\Linker::substitute() does only seem to accept object<phpDocumentor\Des...escriptorAbstract>|null , maybe add an additional type check?
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: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.
Loading history...
|
|||
174 | |||
175 | // if the returned response is not an object it must be grafted on the calling object |
||
176 | 4 | if ($response !== null) { |
|
177 | 4 | $setter = 'set' . ucfirst($fieldName); |
|
178 | 4 | $item->{$setter}($response); |
|
179 | } |
||
180 | } |
||
181 | } |
||
182 | |||
183 | 7 | return $result; |
|
184 | } |
||
185 | |||
186 | /** |
||
187 | * Attempts to find a Descriptor object alias with the FQSEN of the element it represents. |
||
188 | * |
||
189 | * This method will try to fetch an element after normalizing the provided FQSEN. The FQSEN may contain references |
||
190 | * (bindings) that can only be resolved during linking (such as `self`) or it may contain a context marker |
||
191 | * {@see CONTEXT_MARKER}. |
||
192 | * |
||
193 | * If there is a context marker then this method will see if a child of the given container exists that matches the |
||
194 | * element following the marker. If such a child does not exist in the current container then the namespace is |
||
195 | * queried if a child exists there that matches. |
||
196 | * |
||
197 | * For example: |
||
198 | * |
||
199 | * Given the Fqsen `@context::myFunction()` and the lastContainer `\My\Class` will this method first check |
||
200 | * to see if `\My\Class::myFunction()` exists; if it doesn't it will then check if `\My\myFunction()` exists. |
||
201 | * |
||
202 | * If neither element exists then this method assumes it is an undocumented class/trait/interface and change the |
||
203 | * given FQSEN by returning the namespaced element name (thus in the example above that would be |
||
204 | * `\My\myFunction()`). The calling method {@see substitute()} will then replace the value of the field containing |
||
205 | * the context marker with this normalized string. |
||
206 | * |
||
207 | * @param string $fqsen |
||
208 | * @param DescriptorAbstract|null $container |
||
209 | * |
||
210 | * @return DescriptorAbstract|string|null |
||
211 | */ |
||
212 | 5 | public function findAlias($fqsen, $container = null) |
|
213 | { |
||
214 | 5 | $fqsen = $this->replacePseudoTypes($fqsen, $container); |
|
215 | |||
216 | 5 | if ($this->isContextMarkerInFqsen($fqsen) && $container instanceof DescriptorAbstract) { |
|
217 | // first exchange `@context::element` for `\My\Class::element` and if it exists, return that |
||
218 | 4 | $classMember = $this->fetchElementByFqsen($this->getTypeWithClassAsContext($fqsen, $container)); |
|
219 | 4 | if ($classMember) { |
|
220 | 1 | return $classMember; |
|
221 | } |
||
222 | |||
223 | // otherwise exchange `@context::element` for `\My\element` and if it exists, return that |
||
224 | 3 | $namespaceContext = $this->getTypeWithNamespaceAsContext($fqsen, $container); |
|
225 | 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...
|
|||
226 | 3 | if ($namespaceMember) { |
|
227 | 1 | return $namespaceMember; |
|
228 | } |
||
229 | |||
230 | // otherwise check if the element exists in the global namespace and if it exists, return that |
||
231 | 2 | $globalNamespaceContext = $this->getTypeWithGlobalNamespaceAsContext($fqsen); |
|
232 | 2 | $globalNamespaceMember = $this->fetchElementByFqsen($globalNamespaceContext); |
|
233 | 2 | if ($globalNamespaceMember) { |
|
234 | 1 | return $globalNamespaceMember; |
|
235 | } |
||
236 | |||
237 | // Otherwise we assume it is an undocumented class/interface/trait and return `\My\element` so |
||
238 | // that the name containing the marker may be replaced by the class reference as string |
||
239 | 1 | return $namespaceContext; |
|
240 | } |
||
241 | |||
242 | 1 | return $this->fetchElementByFqsen($fqsen); |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * Returns the value of a field in the given object. |
||
247 | * |
||
248 | * @param object $object |
||
249 | * @param string $fieldName |
||
250 | * |
||
251 | * @return string|object |
||
252 | */ |
||
253 | 1 | public function findFieldValue($object, $fieldName) |
|
254 | { |
||
255 | 1 | $getter = 'get' . ucfirst($fieldName); |
|
256 | |||
257 | 1 | return $object->{$getter}(); |
|
258 | } |
||
259 | |||
260 | /** |
||
261 | * Returns true if the given Descriptor is a container type. |
||
262 | * |
||
263 | * @param DescriptorAbstract|mixed $item |
||
264 | * |
||
265 | * @return bool |
||
266 | */ |
||
267 | 1 | protected function isDescriptorContainer($item) |
|
268 | { |
||
269 | 1 | return $item instanceof FileDescriptor |
|
270 | 1 | || $item instanceof NamespaceDescriptor |
|
271 | 1 | || $item instanceof ClassDescriptor |
|
272 | 1 | || $item instanceof TraitDescriptor |
|
273 | 1 | || $item instanceof InterfaceDescriptor; |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Replaces pseudo-types, such as `self`, into a normalized version based on the last container that was |
||
278 | * encountered. |
||
279 | * |
||
280 | * @param string $fqsen |
||
281 | * @param DescriptorAbstract|null $container |
||
282 | * |
||
283 | * @return string |
||
284 | */ |
||
285 | 2 | protected function replacePseudoTypes($fqsen, $container) |
|
286 | { |
||
287 | 2 | $pseudoTypes = ['self', '$this']; |
|
288 | 2 | foreach ($pseudoTypes as $pseudoType) { |
|
289 | 2 | if ((strpos($fqsen, $pseudoType . '::') === 0 || $fqsen === $pseudoType) && $container) { |
|
290 | 2 | $fqsen = $container->getFullyQualifiedStructuralElementName() |
|
291 | 2 | . substr($fqsen, strlen($pseudoType)); |
|
292 | } |
||
293 | } |
||
294 | |||
295 | 2 | return $fqsen; |
|
296 | } |
||
297 | |||
298 | /** |
||
299 | * Returns true if the context marker is found in the given FQSEN. |
||
300 | * |
||
301 | * @param string $fqsen |
||
302 | * |
||
303 | * @return bool |
||
304 | */ |
||
305 | 4 | protected function isContextMarkerInFqsen($fqsen) |
|
306 | { |
||
307 | 4 | return strpos($fqsen, self::CONTEXT_MARKER) !== false; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
312 | * |
||
313 | * @param string $fqsen |
||
314 | * @return string |
||
315 | */ |
||
316 | 2 | protected function getTypeWithClassAsContext($fqsen, DescriptorAbstract $container) |
|
317 | { |
||
318 | 2 | if (!$container instanceof ClassDescriptor |
|
319 | 2 | && !$container instanceof InterfaceDescriptor |
|
320 | 2 | && !$container instanceof TraitDescriptor |
|
321 | ) { |
||
322 | 1 | return $fqsen; |
|
323 | } |
||
324 | |||
325 | 1 | $containerFqsen = $container->getFullyQualifiedStructuralElementName(); |
|
326 | |||
327 | 1 | return str_replace(self::CONTEXT_MARKER . '::', $containerFqsen . '::', $fqsen); |
|
328 | } |
||
329 | |||
330 | /** |
||
331 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
332 | * |
||
333 | * @param string $fqsen |
||
334 | * @return string |
||
335 | */ |
||
336 | 2 | protected function getTypeWithNamespaceAsContext($fqsen, DescriptorAbstract $container) |
|
337 | { |
||
338 | 2 | $namespace = $container instanceof NamespaceDescriptor ? $container : $container->getNamespace(); |
|
339 | 2 | $fqnn = $namespace instanceof NamespaceDescriptor |
|
340 | 1 | ? $namespace->getFullyQualifiedStructuralElementName() |
|
341 | 2 | : $namespace; |
|
342 | |||
343 | 2 | return str_replace(self::CONTEXT_MARKER . '::', $fqnn . '\\', $fqsen); |
|
344 | } |
||
345 | |||
346 | /** |
||
347 | * Normalizes the given FQSEN as if the context marker represents the global namespace as parent. |
||
348 | * |
||
349 | * @param string $fqsen |
||
350 | * @return string |
||
351 | */ |
||
352 | 2 | protected function getTypeWithGlobalNamespaceAsContext($fqsen) |
|
353 | { |
||
354 | 2 | return str_replace(self::CONTEXT_MARKER . '::', '\\', $fqsen); |
|
355 | } |
||
356 | |||
357 | /** |
||
358 | * Attempts to find an element with the given Fqsen in the list of elements for this project and returns null if |
||
359 | * it cannot find it. |
||
360 | * |
||
361 | * @param string $fqsen |
||
362 | * |
||
363 | * @return DescriptorAbstract|null |
||
364 | */ |
||
365 | 4 | protected function fetchElementByFqsen($fqsen) |
|
366 | { |
||
367 | 4 | return $this->elementList[$fqsen] ?? null; |
|
368 | } |
||
369 | } |
||
370 |
Since your code implements the magic getter
_get
, this function will be called for any read access on an undefined variable. You can add the@property
annotation to your class or interface to document the existence of this variable.If the property has read access only, you can use the @property-read annotation instead.
Of course, you may also just have mistyped another name, in which case you should fix the error.
See also the PhpDoc documentation for @property.