Completed
Push — develop ( 722f70...af048b )
by Jaap
15:12 queued 05:04
created

src/phpDocumentor/Compiler/Linker/Linker.php (1 issue)

Upgrade to new PHP Analysis Engine

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
0 ignored issues
show
This class has a complexity of 50 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

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