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); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.