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

src/phpDocumentor/Compiler/Linker/Linker.php (7 issues)

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());
0 ignored issues
show
The property elements does not exist on object<phpDocumentor\Descriptor\Collection>. Since you implemented __get, maybe consider adding a @property annotation.

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.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

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.

Loading history...
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);
0 ignored issues
show
It seems like $newContainer defined by $this->isDescriptorConta...m) ? $item : $container on line 167 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...
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);
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 getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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);
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $globalNamespaceContext exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
237
            $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 getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Comprehensibility Naming introduced by
The variable name $globalNamespaceMember exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

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