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