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