Completed
Push — develop ( 71fd61...bbac44 )
by Jaap
06:03 queued 02:27
created

src/phpDocumentor/Compiler/Linker/Linker.php (4 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
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
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 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);
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...
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);
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...
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);
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...
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