Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
62.50% covered (warning)
62.50%
5 / 8
CRAP
94.62% covered (success)
94.62%
88 / 93
PhpDocExtractor
0.00% covered (danger)
0.00%
0 / 1
62.50% covered (warning)
62.50%
5 / 8
51.40
94.62% covered (success)
94.62%
88 / 93
 __construct
0.00% covered (danger)
0.00%
0 / 1
6.05
88.89% covered (warning)
88.89%
8 / 9
 getShortDescription
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
12 / 12
 getLongDescription
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 getTypes
100.00% covered (success)
100.00%
1 / 1
11
100.00% covered (success)
100.00%
21 / 21
 getDocBlock
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
15 / 15
 getDocBlockFromProperty
0.00% covered (danger)
0.00%
0 / 1
4.25
75.00% covered (warning)
75.00%
6 / 8
 getDocBlockFromMethod
0.00% covered (danger)
0.00%
0 / 1
12.20
88.89% covered (warning)
88.89%
16 / 18
 createFromReflector
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\PropertyInfo\Extractor;
13
14use phpDocumentor\Reflection\DocBlock;
15use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
16use phpDocumentor\Reflection\DocBlockFactory;
17use phpDocumentor\Reflection\DocBlockFactoryInterface;
18use phpDocumentor\Reflection\Types\Context;
19use phpDocumentor\Reflection\Types\ContextFactory;
20use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
21use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
22use Symfony\Component\PropertyInfo\Type;
23use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
24
25/**
26 * Extracts data using a PHPDoc parser.
27 *
28 * @author K√©vin Dunglas <dunglas@gmail.com>
29 *
30 * @final
31 */
32class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
33{
34    const PROPERTY = 0;
35    const ACCESSOR = 1;
36    const MUTATOR = 2;
37
38    /**
39     * @var DocBlock[]
40     */
41    private $docBlocks = [];
42
43    /**
44     * @var Context[]
45     */
46    private $contexts = [];
47
48    private $docBlockFactory;
49    private $contextFactory;
50    private $phpDocTypeHelper;
51    private $mutatorPrefixes;
52    private $accessorPrefixes;
53    private $arrayMutatorPrefixes;
54
55    /**
56     * @param string[]|null $mutatorPrefixes
57     * @param string[]|null $accessorPrefixes
58     * @param string[]|null $arrayMutatorPrefixes
59     */
60    public function __construct(DocBlockFactoryInterface $docBlockFactory = null, array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null)
61    {
62        if (!class_exists(DocBlockFactory::class)) {
63            throw new \LogicException(sprintf('Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed.', __CLASS__));
64        }
65
66        $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance();
67        $this->contextFactory = new ContextFactory();
68        $this->phpDocTypeHelper = new PhpDocTypeHelper();
69        $this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : ReflectionExtractor::$defaultMutatorPrefixes;
70        $this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : ReflectionExtractor::$defaultAccessorPrefixes;
71        $this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : ReflectionExtractor::$defaultArrayMutatorPrefixes;
72    }
73
74    /**
75     * {@inheritdoc}
76     */
77    public function getShortDescription(string $class, string $property, array $context = []): ?string
78    {
79        /** @var $docBlock DocBlock */
80        list($docBlock) = $this->getDocBlock($class, $property);
81        if (!$docBlock) {
82            return null;
83        }
84
85        $shortDescription = $docBlock->getSummary();
86
87        if (!empty($shortDescription)) {
88            return $shortDescription;
89        }
90
91        foreach ($docBlock->getTagsByName('var') as $var) {
92            if ($var && !$var instanceof InvalidTag) {
93                $varDescription = $var->getDescription()->render();
94
95                if (!empty($varDescription)) {
96                    return $varDescription;
97                }
98            }
99        }
100
101        return null;
102    }
103
104    /**
105     * {@inheritdoc}
106     */
107    public function getLongDescription(string $class, string $property, array $context = []): ?string
108    {
109        /** @var $docBlock DocBlock */
110        list($docBlock) = $this->getDocBlock($class, $property);
111        if (!$docBlock) {
112            return null;
113        }
114
115        $contents = $docBlock->getDescription()->render();
116
117        return '' === $contents ? null : $contents;
118    }
119
120    /**
121     * {@inheritdoc}
122     */
123    public function getTypes(string $class, string $property, array $context = []): ?array
124    {
125        /** @var $docBlock DocBlock */
126        list($docBlock, $source, $prefix) = $this->getDocBlock($class, $property);
127        if (!$docBlock) {
128            return null;
129        }
130
131        switch ($source) {
132            case self::PROPERTY:
133                $tag = 'var';
134                break;
135
136            case self::ACCESSOR:
137                $tag = 'return';
138                break;
139
140            case self::MUTATOR:
141                $tag = 'param';
142                break;
143        }
144
145        $types = [];
146        /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
147        foreach ($docBlock->getTagsByName($tag) as $tag) {
148            if ($tag && !$tag instanceof InvalidTag && null !== $tag->getType()) {
149                $types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
150            }
151        }
152
153        if (!isset($types[0])) {
154            return null;
155        }
156
157        if (!\in_array($prefix, $this->arrayMutatorPrefixes)) {
158            return $types;
159        }
160
161        return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
162    }
163
164    private function getDocBlock(string $class, string $property): array
165    {
166        $propertyHash = sprintf('%s::%s', $class, $property);
167
168        if (isset($this->docBlocks[$propertyHash])) {
169            return $this->docBlocks[$propertyHash];
170        }
171
172        $ucFirstProperty = ucfirst($property);
173
174        switch (true) {
175            case $docBlock = $this->getDocBlockFromProperty($class, $property):
176                $data = [$docBlock, self::PROPERTY, null];
177                break;
178
179            case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR):
180                $data = [$docBlock, self::ACCESSOR, null];
181                break;
182
183            case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR):
184                $data = [$docBlock, self::MUTATOR, $prefix];
185                break;
186
187            default:
188                $data = [null, null, null];
189        }
190
191        return $this->docBlocks[$propertyHash] = $data;
192    }
193
194    private function getDocBlockFromProperty(string $class, string $property): ?DocBlock
195    {
196        // Use a ReflectionProperty instead of $class to get the parent class if applicable
197        try {
198            $reflectionProperty = new \ReflectionProperty($class, $property);
199        } catch (\ReflectionException $e) {
200            return null;
201        }
202
203        try {
204            return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass()));
205        } catch (\InvalidArgumentException $e) {
206            return null;
207        } catch (\RuntimeException $e) {
208            return null;
209        }
210    }
211
212    private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array
213    {
214        $prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
215        $prefix = null;
216
217        foreach ($prefixes as $prefix) {
218            $methodName = $prefix.$ucFirstProperty;
219
220            try {
221                $reflectionMethod = new \ReflectionMethod($class, $methodName);
222                if ($reflectionMethod->isStatic()) {
223                    continue;
224                }
225
226                if (
227                    (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) ||
228                    (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
229                ) {
230                    break;
231                }
232            } catch (\ReflectionException $e) {
233                // Try the next prefix if the method doesn't exist
234            }
235        }
236
237        if (!isset($reflectionMethod)) {
238            return null;
239        }
240
241        try {
242            return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix];
243        } catch (\InvalidArgumentException $e) {
244            return null;
245        } catch (\RuntimeException $e) {
246            return null;
247        }
248    }
249
250    /**
251     * Prevents a lot of redundant calls to ContextFactory::createForNamespace().
252     */
253    private function createFromReflector(\ReflectionClass $reflector): Context
254    {
255        $cacheKey = $reflector->getNamespaceName().':'.$reflector->getFileName();
256
257        if (isset($this->contexts[$cacheKey])) {
258            return $this->contexts[$cacheKey];
259        }
260
261        $this->contexts[$cacheKey] = $this->contextFactory->createFromReflector($reflector);
262
263        return $this->contexts[$cacheKey];
264    }
265}