/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

https://github.com/deviantintegral/symfony · PHP · 347 lines · 220 code · 59 blank · 68 comment · 31 complexity · 5cb64350a031c85382f5d232dd7100ea MD5 · raw file

  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\PropertyInfo\Extractor;
  11. use Symfony\Component\Inflector\Inflector;
  12. use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
  13. use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
  14. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  15. use Symfony\Component\PropertyInfo\Type;
  16. /**
  17. * Extracts data using the reflection API.
  18. *
  19. * @author Kévin Dunglas <dunglas@gmail.com>
  20. *
  21. * @final
  22. */
  23. class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
  24. {
  25. /**
  26. * @internal
  27. */
  28. public static $defaultMutatorPrefixes = array('add', 'remove', 'set');
  29. /**
  30. * @internal
  31. */
  32. public static $defaultAccessorPrefixes = array('is', 'can', 'get', 'has');
  33. /**
  34. * @internal
  35. */
  36. public static $defaultArrayMutatorPrefixes = array('add', 'remove');
  37. private $mutatorPrefixes;
  38. private $accessorPrefixes;
  39. private $arrayMutatorPrefixes;
  40. private $enableConstructorExtraction;
  41. /**
  42. * @param string[]|null $mutatorPrefixes
  43. * @param string[]|null $accessorPrefixes
  44. * @param string[]|null $arrayMutatorPrefixes
  45. */
  46. public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true)
  47. {
  48. $this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : self::$defaultMutatorPrefixes;
  49. $this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : self::$defaultAccessorPrefixes;
  50. $this->arrayMutatorPrefixes = null !== $arrayMutatorPrefixes ? $arrayMutatorPrefixes : self::$defaultArrayMutatorPrefixes;
  51. $this->enableConstructorExtraction = $enableConstructorExtraction;
  52. }
  53. /**
  54. * {@inheritdoc}
  55. */
  56. public function getProperties($class, array $context = array())
  57. {
  58. try {
  59. $reflectionClass = new \ReflectionClass($class);
  60. } catch (\ReflectionException $e) {
  61. return;
  62. }
  63. $reflectionProperties = $reflectionClass->getProperties();
  64. $properties = array();
  65. foreach ($reflectionProperties as $reflectionProperty) {
  66. if ($reflectionProperty->isPublic()) {
  67. $properties[$reflectionProperty->name] = $reflectionProperty->name;
  68. }
  69. }
  70. foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
  71. if ($reflectionMethod->isStatic()) {
  72. continue;
  73. }
  74. $propertyName = $this->getPropertyName($reflectionMethod->name, $reflectionProperties);
  75. if (!$propertyName || isset($properties[$propertyName])) {
  76. continue;
  77. }
  78. if (!$reflectionClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName)) {
  79. $propertyName = lcfirst($propertyName);
  80. }
  81. $properties[$propertyName] = $propertyName;
  82. }
  83. return $properties ? array_values($properties) : null;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function getTypes($class, $property, array $context = array())
  89. {
  90. if ($fromMutator = $this->extractFromMutator($class, $property)) {
  91. return $fromMutator;
  92. }
  93. if ($fromAccessor = $this->extractFromAccessor($class, $property)) {
  94. return $fromAccessor;
  95. }
  96. if (
  97. $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction &&
  98. $fromConstructor = $this->extractFromConstructor($class, $property)
  99. ) {
  100. return $fromConstructor;
  101. }
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. public function isReadable($class, $property, array $context = array())
  107. {
  108. if ($this->isPublicProperty($class, $property)) {
  109. return true;
  110. }
  111. list($reflectionMethod) = $this->getAccessorMethod($class, $property);
  112. return null !== $reflectionMethod;
  113. }
  114. /**
  115. * {@inheritdoc}
  116. */
  117. public function isWritable($class, $property, array $context = array())
  118. {
  119. if ($this->isPublicProperty($class, $property)) {
  120. return true;
  121. }
  122. list($reflectionMethod) = $this->getMutatorMethod($class, $property);
  123. return null !== $reflectionMethod;
  124. }
  125. /**
  126. * @return Type[]|null
  127. */
  128. private function extractFromMutator(string $class, string $property): ?array
  129. {
  130. list($reflectionMethod, $prefix) = $this->getMutatorMethod($class, $property);
  131. if (null === $reflectionMethod) {
  132. return null;
  133. }
  134. $reflectionParameters = $reflectionMethod->getParameters();
  135. $reflectionParameter = $reflectionParameters[0];
  136. if (!$reflectionType = $reflectionParameter->getType()) {
  137. return null;
  138. }
  139. $type = $this->extractFromReflectionType($reflectionType);
  140. if (in_array($prefix, $this->arrayMutatorPrefixes)) {
  141. $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type);
  142. }
  143. return array($type);
  144. }
  145. /**
  146. * Tries to extract type information from accessors.
  147. *
  148. * @return Type[]|null
  149. */
  150. private function extractFromAccessor(string $class, string $property): ?array
  151. {
  152. list($reflectionMethod, $prefix) = $this->getAccessorMethod($class, $property);
  153. if (null === $reflectionMethod) {
  154. return null;
  155. }
  156. if ($reflectionType = $reflectionMethod->getReturnType()) {
  157. return array($this->extractFromReflectionType($reflectionType));
  158. }
  159. if (in_array($prefix, array('is', 'can', 'has'))) {
  160. return array(new Type(Type::BUILTIN_TYPE_BOOL));
  161. }
  162. return null;
  163. }
  164. /**
  165. * Tries to extract type information from constructor.
  166. *
  167. * @return Type[]|null
  168. */
  169. private function extractFromConstructor(string $class, string $property): ?array
  170. {
  171. try {
  172. $reflectionClass = new \ReflectionClass($class);
  173. } catch (\ReflectionException $e) {
  174. return null;
  175. }
  176. $constructor = $reflectionClass->getConstructor();
  177. if (!$constructor) {
  178. return null;
  179. }
  180. foreach ($constructor->getParameters() as $parameter) {
  181. if ($property !== $parameter->name) {
  182. continue;
  183. }
  184. return array($this->extractFromReflectionType($parameter->getType()));
  185. }
  186. if ($parentClass = $reflectionClass->getParentClass()) {
  187. return $this->extractFromConstructor($parentClass->getName(), $property);
  188. }
  189. return null;
  190. }
  191. private function extractFromReflectionType(\ReflectionType $reflectionType): Type
  192. {
  193. $phpTypeOrClass = $reflectionType->getName();
  194. $nullable = $reflectionType->allowsNull();
  195. if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
  196. $type = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
  197. } elseif ('void' === $phpTypeOrClass) {
  198. $type = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
  199. } elseif ($reflectionType->isBuiltin()) {
  200. $type = new Type($phpTypeOrClass, $nullable);
  201. } else {
  202. $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $phpTypeOrClass);
  203. }
  204. return $type;
  205. }
  206. private function isPublicProperty(string $class, string $property): bool
  207. {
  208. try {
  209. $reflectionProperty = new \ReflectionProperty($class, $property);
  210. return $reflectionProperty->isPublic();
  211. } catch (\ReflectionException $e) {
  212. // Return false if the property doesn't exist
  213. }
  214. return false;
  215. }
  216. /**
  217. * Gets the accessor method.
  218. *
  219. * Returns an array with a the instance of \ReflectionMethod as first key
  220. * and the prefix of the method as second or null if not found.
  221. */
  222. private function getAccessorMethod(string $class, string $property): ?array
  223. {
  224. $ucProperty = ucfirst($property);
  225. foreach ($this->accessorPrefixes as $prefix) {
  226. try {
  227. $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty);
  228. if ($reflectionMethod->isStatic()) {
  229. continue;
  230. }
  231. if (0 === $reflectionMethod->getNumberOfRequiredParameters()) {
  232. return array($reflectionMethod, $prefix);
  233. }
  234. } catch (\ReflectionException $e) {
  235. // Return null if the property doesn't exist
  236. }
  237. }
  238. return null;
  239. }
  240. /**
  241. * Returns an array with a the instance of \ReflectionMethod as first key
  242. * and the prefix of the method as second or null if not found.
  243. */
  244. private function getMutatorMethod(string $class, string $property): ?array
  245. {
  246. $ucProperty = ucfirst($property);
  247. $ucSingulars = (array) Inflector::singularize($ucProperty);
  248. foreach ($this->mutatorPrefixes as $prefix) {
  249. $names = array($ucProperty);
  250. if (in_array($prefix, $this->arrayMutatorPrefixes)) {
  251. $names = array_merge($names, $ucSingulars);
  252. }
  253. foreach ($names as $name) {
  254. try {
  255. $reflectionMethod = new \ReflectionMethod($class, $prefix.$name);
  256. if ($reflectionMethod->isStatic()) {
  257. continue;
  258. }
  259. // Parameter can be optional to allow things like: method(array $foo = null)
  260. if ($reflectionMethod->getNumberOfParameters() >= 1) {
  261. return array($reflectionMethod, $prefix);
  262. }
  263. } catch (\ReflectionException $e) {
  264. // Try the next prefix if the method doesn't exist
  265. }
  266. }
  267. }
  268. return null;
  269. }
  270. private function getPropertyName(string $methodName, array $reflectionProperties): ?string
  271. {
  272. $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes));
  273. if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) {
  274. if (!in_array($matches[1], $this->arrayMutatorPrefixes)) {
  275. return $matches[2];
  276. }
  277. foreach ($reflectionProperties as $reflectionProperty) {
  278. foreach ((array) Inflector::singularize($reflectionProperty->name) as $name) {
  279. if (strtolower($name) === strtolower($matches[2])) {
  280. return $reflectionProperty->name;
  281. }
  282. }
  283. }
  284. return $matches[2];
  285. }
  286. return null;
  287. }
  288. }