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

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

Check for undocumented magic property with read access

Documentation Informational

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