Check for undocumented magic property with read access
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 | 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 | * @return void |
||
81 | */ |
||
82 | 1 | public function execute(ProjectDescriptor $project) |
|
83 | { |
||
84 | 1 | $this->setObjectAliasesList($project->getIndexes()->elements->getAll()); |
|
0 ignored issues
–
show
|
|||
85 | 1 | $this->substitute($project); |
|
86 | 1 | } |
|
87 | |||
88 | /** |
||
89 | * Returns the list of substitutions for the linker. |
||
90 | * |
||
91 | * @return string[] |
||
92 | */ |
||
93 | 1 | public function getSubstitutions() |
|
94 | { |
||
95 | 1 | 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 | 3 | public function setObjectAliasesList(array $elementList) |
|
106 | { |
||
107 | 3 | $this->elementList = $elementList; |
|
108 | 3 | } |
|
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 | 7 | public function substitute($item, $container = null) |
|
139 | { |
||
140 | 7 | $result = null; |
|
141 | |||
142 | 7 | if (is_string($item)) { |
|
143 | 5 | $result = $this->findAlias($item, $container); |
|
144 | 7 | } elseif (is_array($item) || ($item instanceof \Traversable && ! $item instanceof ProjectInterface)) { |
|
145 | 2 | $isModified = false; |
|
146 | 2 | foreach ($item as $key => $element) { |
|
147 | 2 | $isModified = true; |
|
148 | |||
149 | 2 | $element = $this->substitute($element, $container); |
|
150 | 2 | if ($element !== null) { |
|
151 | 2 | $item[$key] = $element; |
|
152 | } |
||
153 | } |
||
154 | 2 | if ($isModified) { |
|
155 | 2 | $result = $item; |
|
156 | } |
||
157 | 6 | } elseif (is_object($item) && $item instanceof UnknownTypeDescriptor) { |
|
158 | 1 | $alias = $this->findAlias($item->getName()); |
|
159 | 1 | $result = $alias ?: $item; |
|
160 | 5 | } elseif (is_object($item)) { |
|
161 | 5 | $hash = spl_object_hash($item); |
|
162 | 5 | if (isset($this->processedObjects[$hash])) { |
|
163 | // if analyzed; just return |
||
164 | 1 | return null; |
|
165 | } |
||
166 | |||
167 | 5 | $newContainer = ($this->isDescriptorContainer($item)) ? $item : $container; |
|
168 | |||
169 | 5 | $this->processedObjects[$hash] = true; |
|
170 | |||
171 | 5 | $objectClassName = get_class($item); |
|
172 | 5 | $fieldNames = $this->substitutions[$objectClassName] ?? array(); |
|
173 | |||
174 | 5 | foreach ($fieldNames as $fieldName) { |
|
175 | 4 | $fieldValue = $this->findFieldValue($item, $fieldName); |
|
176 | 4 | $response = $this->substitute($fieldValue, $newContainer); |
|
177 | |||
178 | // if the returned response is not an object it must be grafted on the calling object |
||
179 | 4 | if ($response !== null) { |
|
180 | 4 | $setter = 'set'.ucfirst($fieldName); |
|
181 | 4 | $item->$setter($response); |
|
182 | } |
||
183 | } |
||
184 | } |
||
185 | |||
186 | 7 | return $result; |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * Attempts to find a Descriptor object alias with the FQSEN of the element it represents. |
||
191 | * |
||
192 | * This method will try to fetch an element after normalizing the provided FQSEN. The FQSEN may contain references |
||
193 | * (bindings) that can only be resolved during linking (such as `self`) or it may contain a context marker |
||
194 | * {@see CONTEXT_MARKER}. |
||
195 | * |
||
196 | * If there is a context marker then this method will see if a child of the given container exists that matches the |
||
197 | * element following the marker. If such a child does not exist in the current container then the namespace is |
||
198 | * queried if a child exists there that matches. |
||
199 | * |
||
200 | * For example: |
||
201 | * |
||
202 | * Given the Fqsen `@context::myFunction()` and the lastContainer `\My\Class` will this method first check |
||
203 | * to see if `\My\Class::myFunction()` exists; if it doesn't it will then check if `\My\myFunction()` exists. |
||
204 | * |
||
205 | * If neither element exists then this method assumes it is an undocumented class/trait/interface and change the |
||
206 | * given FQSEN by returning the namespaced element name (thus in the example above that would be |
||
207 | * `\My\myFunction()`). The calling method {@see substitute()} will then replace the value of the field containing |
||
208 | * the context marker with this normalized string. |
||
209 | * |
||
210 | * @param string $fqsen |
||
211 | * @param DescriptorAbstract|null $container |
||
212 | * |
||
213 | * @return DescriptorAbstract|string|null |
||
214 | */ |
||
215 | 5 | public function findAlias($fqsen, $container = null) |
|
216 | { |
||
217 | 5 | $fqsen = $this->replacePseudoTypes($fqsen, $container); |
|
218 | |||
219 | 5 | if ($this->isContextMarkerInFqsen($fqsen) && $container instanceof DescriptorAbstract) { |
|
220 | // first exchange `@context::element` for `\My\Class::element` and if it exists, return that |
||
221 | 4 | $classMember = $this->fetchElementByFqsen($this->getTypeWithClassAsContext($fqsen, $container)); |
|
222 | 4 | if ($classMember) { |
|
223 | 1 | return $classMember; |
|
224 | } |
||
225 | |||
226 | // otherwise exchange `@context::element` for `\My\element` and if it exists, return that |
||
227 | 3 | $namespaceContext = $this->getTypeWithNamespaceAsContext($fqsen, $container); |
|
228 | 3 | $namespaceMember = $this->fetchElementByFqsen($namespaceContext); |
|
229 | 3 | if ($namespaceMember) { |
|
230 | 1 | return $namespaceMember; |
|
231 | } |
||
232 | |||
233 | // otherwise check if the element exists in the global namespace and if it exists, return that |
||
234 | 2 | $globalNamespaceContext = $this->getTypeWithGlobalNamespaceAsContext($fqsen); |
|
235 | 2 | $globalNamespaceMember = $this->fetchElementByFqsen($globalNamespaceContext); |
|
236 | 2 | if ($globalNamespaceMember) { |
|
237 | 1 | return $globalNamespaceMember; |
|
238 | } |
||
239 | |||
240 | // Otherwise we assume it is an undocumented class/interface/trait and return `\My\element` so |
||
241 | // that the name containing the marker may be replaced by the class reference as string |
||
242 | 1 | return $namespaceContext; |
|
243 | } |
||
244 | |||
245 | 1 | return $this->fetchElementByFqsen($fqsen); |
|
246 | } |
||
247 | |||
248 | /** |
||
249 | * Returns the value of a field in the given object. |
||
250 | * |
||
251 | * @param object $object |
||
252 | * @param string $fieldName |
||
253 | * |
||
254 | * @return string|object |
||
255 | */ |
||
256 | 1 | public function findFieldValue($object, $fieldName) |
|
257 | { |
||
258 | 1 | $getter = 'get'.ucfirst($fieldName); |
|
259 | |||
260 | 1 | return $object->$getter(); |
|
261 | } |
||
262 | |||
263 | /** |
||
264 | * Returns true if the given Descriptor is a container type. |
||
265 | * |
||
266 | * @param DescriptorAbstract|mixed $item |
||
267 | * |
||
268 | * @return bool |
||
269 | */ |
||
270 | 1 | protected function isDescriptorContainer($item) |
|
271 | { |
||
272 | 1 | return $item instanceof FileDescriptor |
|
273 | 1 | || $item instanceof NamespaceDescriptor |
|
274 | 1 | || $item instanceof ClassDescriptor |
|
275 | 1 | || $item instanceof TraitDescriptor |
|
276 | 1 | || $item instanceof InterfaceDescriptor; |
|
277 | } |
||
278 | |||
279 | /** |
||
280 | * Replaces pseudo-types, such as `self`, into a normalized version based on the last container that was |
||
281 | * encountered. |
||
282 | * |
||
283 | * @param string $fqsen |
||
284 | * @param DescriptorAbstract|null $container |
||
285 | * |
||
286 | * @return string |
||
287 | */ |
||
288 | 2 | protected function replacePseudoTypes($fqsen, $container) |
|
289 | { |
||
290 | 2 | $pseudoTypes = array('self', '$this'); |
|
291 | 2 | foreach ($pseudoTypes as $pseudoType) { |
|
292 | 2 | if ((strpos($fqsen, $pseudoType . '::') === 0 || $fqsen === $pseudoType) && $container) { |
|
293 | 2 | $fqsen = $container->getFullyQualifiedStructuralElementName() |
|
294 | 2 | . substr($fqsen, strlen($pseudoType)); |
|
295 | } |
||
296 | } |
||
297 | |||
298 | 2 | return $fqsen; |
|
299 | } |
||
300 | |||
301 | /** |
||
302 | * Returns true if the context marker is found in the given FQSEN. |
||
303 | * |
||
304 | * @param string $fqsen |
||
305 | * |
||
306 | * @return bool |
||
307 | */ |
||
308 | 4 | protected function isContextMarkerInFqsen($fqsen) |
|
309 | { |
||
310 | 4 | return strpos($fqsen, self::CONTEXT_MARKER) !== false; |
|
311 | } |
||
312 | |||
313 | /** |
||
314 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
315 | * |
||
316 | * @param string $fqsen |
||
317 | * @param DescriptorAbstract $container |
||
318 | * |
||
319 | * @return string |
||
320 | */ |
||
321 | 2 | protected function getTypeWithClassAsContext($fqsen, DescriptorAbstract $container) |
|
322 | { |
||
323 | 2 | if (!$container instanceof ClassDescriptor |
|
324 | 2 | && !$container instanceof InterfaceDescriptor |
|
325 | 2 | && !$container instanceof TraitDescriptor |
|
326 | ) { |
||
327 | 1 | return $fqsen; |
|
328 | } |
||
329 | |||
330 | 1 | $containerFqsen = $container->getFullyQualifiedStructuralElementName(); |
|
331 | |||
332 | 1 | return str_replace(self::CONTEXT_MARKER . '::', $containerFqsen . '::', $fqsen); |
|
333 | } |
||
334 | |||
335 | /** |
||
336 | * Normalizes the given FQSEN as if the context marker represents a class/interface/trait as parent. |
||
337 | * |
||
338 | * @param string $fqsen |
||
339 | * @param DescriptorAbstract $container |
||
340 | * |
||
341 | * @return string |
||
342 | */ |
||
343 | 2 | protected function getTypeWithNamespaceAsContext($fqsen, DescriptorAbstract $container) |
|
344 | { |
||
345 | 2 | $namespace = $container instanceof NamespaceDescriptor ? $container : $container->getNamespace(); |
|
346 | 2 | $fqnn = $namespace instanceof NamespaceDescriptor |
|
347 | 1 | ? $namespace->getFullyQualifiedStructuralElementName() |
|
348 | 2 | : $namespace; |
|
349 | |||
350 | 2 | return str_replace(self::CONTEXT_MARKER . '::', $fqnn . '\\', $fqsen); |
|
351 | } |
||
352 | |||
353 | /** |
||
354 | * Normalizes the given FQSEN as if the context marker represents the global namespace as parent. |
||
355 | * |
||
356 | * @param string $fqsen |
||
357 | * @return string |
||
358 | */ |
||
359 | 2 | protected function getTypeWithGlobalNamespaceAsContext($fqsen) |
|
360 | { |
||
361 | 2 | return str_replace(self::CONTEXT_MARKER . '::', '\\', $fqsen); |
|
362 | } |
||
363 | |||
364 | /** |
||
365 | * Attempts to find an element with the given Fqsen in the list of elements for this project and returns null if |
||
366 | * it cannot find it. |
||
367 | * |
||
368 | * @param string $fqsen |
||
369 | * |
||
370 | * @return DescriptorAbstract|null |
||
371 | */ |
||
372 | 4 | protected function fetchElementByFqsen($fqsen) |
|
373 | { |
||
374 | 4 | return $this->elementList[$fqsen] ?? null; |
|
375 | } |
||
376 | } |
||
377 |
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.