No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ReflectionExtractor.php 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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\PropertyInfo\PropertyAccessExtractorInterface;
  12. use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
  13. use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
  14. use Symfony\Component\PropertyInfo\PropertyReadInfo;
  15. use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
  16. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  17. use Symfony\Component\PropertyInfo\PropertyWriteInfo;
  18. use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
  19. use Symfony\Component\PropertyInfo\Type;
  20. use Symfony\Component\String\Inflector\EnglishInflector;
  21. use Symfony\Component\String\Inflector\InflectorInterface;
  22. /**
  23. * Extracts data using the reflection API.
  24. *
  25. * @author Kévin Dunglas <dunglas@gmail.com>
  26. *
  27. * @final
  28. */
  29. class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface
  30. {
  31. /**
  32. * @internal
  33. */
  34. public static $defaultMutatorPrefixes = ['add', 'remove', 'set'];
  35. /**
  36. * @internal
  37. */
  38. public static $defaultAccessorPrefixes = ['get', 'is', 'has', 'can'];
  39. /**
  40. * @internal
  41. */
  42. public static $defaultArrayMutatorPrefixes = ['add', 'remove'];
  43. public const ALLOW_PRIVATE = 1;
  44. public const ALLOW_PROTECTED = 2;
  45. public const ALLOW_PUBLIC = 4;
  46. /** @var int Allow none of the magic methods */
  47. public const DISALLOW_MAGIC_METHODS = 0;
  48. /** @var int Allow magic __get methods */
  49. public const ALLOW_MAGIC_GET = 1 << 0;
  50. /** @var int Allow magic __set methods */
  51. public const ALLOW_MAGIC_SET = 1 << 1;
  52. /** @var int Allow magic __call methods */
  53. public const ALLOW_MAGIC_CALL = 1 << 2;
  54. private const MAP_TYPES = [
  55. 'integer' => Type::BUILTIN_TYPE_INT,
  56. 'boolean' => Type::BUILTIN_TYPE_BOOL,
  57. 'double' => Type::BUILTIN_TYPE_FLOAT,
  58. ];
  59. private $mutatorPrefixes;
  60. private $accessorPrefixes;
  61. private $arrayMutatorPrefixes;
  62. private $enableConstructorExtraction;
  63. private $methodReflectionFlags;
  64. private $magicMethodsFlags;
  65. private $propertyReflectionFlags;
  66. private $inflector;
  67. private $arrayMutatorPrefixesFirst;
  68. private $arrayMutatorPrefixesLast;
  69. /**
  70. * @param string[]|null $mutatorPrefixes
  71. * @param string[]|null $accessorPrefixes
  72. * @param string[]|null $arrayMutatorPrefixes
  73. */
  74. public function __construct(?array $mutatorPrefixes = null, ?array $accessorPrefixes = null, ?array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, ?InflectorInterface $inflector = null, int $magicMethodsFlags = self::ALLOW_MAGIC_GET | self::ALLOW_MAGIC_SET)
  75. {
  76. $this->mutatorPrefixes = $mutatorPrefixes ?? self::$defaultMutatorPrefixes;
  77. $this->accessorPrefixes = $accessorPrefixes ?? self::$defaultAccessorPrefixes;
  78. $this->arrayMutatorPrefixes = $arrayMutatorPrefixes ?? self::$defaultArrayMutatorPrefixes;
  79. $this->enableConstructorExtraction = $enableConstructorExtraction;
  80. $this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
  81. $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
  82. $this->magicMethodsFlags = $magicMethodsFlags;
  83. $this->inflector = $inflector ?? new EnglishInflector();
  84. $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes));
  85. $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst);
  86. }
  87. /**
  88. * {@inheritdoc}
  89. */
  90. public function getProperties(string $class, array $context = []): ?array
  91. {
  92. try {
  93. $reflectionClass = new \ReflectionClass($class);
  94. } catch (\ReflectionException $e) {
  95. return null;
  96. }
  97. $reflectionProperties = $reflectionClass->getProperties();
  98. $properties = [];
  99. foreach ($reflectionProperties as $reflectionProperty) {
  100. if ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags) {
  101. $properties[$reflectionProperty->name] = $reflectionProperty->name;
  102. }
  103. }
  104. foreach ($reflectionClass->getMethods($this->methodReflectionFlags) as $reflectionMethod) {
  105. if ($reflectionMethod->isStatic()) {
  106. continue;
  107. }
  108. $propertyName = $this->getPropertyName($reflectionMethod->name, $reflectionProperties);
  109. if (!$propertyName || isset($properties[$propertyName])) {
  110. continue;
  111. }
  112. if ($reflectionClass->hasProperty($lowerCasedPropertyName = lcfirst($propertyName)) || (!$reflectionClass->hasProperty($propertyName) && !preg_match('/^[A-Z]{2,}/', $propertyName))) {
  113. $propertyName = $lowerCasedPropertyName;
  114. }
  115. $properties[$propertyName] = $propertyName;
  116. }
  117. return $properties ? array_values($properties) : null;
  118. }
  119. /**
  120. * {@inheritdoc}
  121. */
  122. public function getTypes(string $class, string $property, array $context = []): ?array
  123. {
  124. if ($fromMutator = $this->extractFromMutator($class, $property)) {
  125. return $fromMutator;
  126. }
  127. if ($fromAccessor = $this->extractFromAccessor($class, $property)) {
  128. return $fromAccessor;
  129. }
  130. if (
  131. ($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction) &&
  132. $fromConstructor = $this->extractFromConstructor($class, $property)
  133. ) {
  134. return $fromConstructor;
  135. }
  136. if ($fromPropertyDeclaration = $this->extractFromPropertyDeclaration($class, $property)) {
  137. return $fromPropertyDeclaration;
  138. }
  139. return null;
  140. }
  141. /**
  142. * {@inheritdoc}
  143. */
  144. public function getTypesFromConstructor(string $class, string $property): ?array
  145. {
  146. try {
  147. $reflection = new \ReflectionClass($class);
  148. } catch (\ReflectionException $e) {
  149. return null;
  150. }
  151. if (!$reflectionConstructor = $reflection->getConstructor()) {
  152. return null;
  153. }
  154. if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
  155. return null;
  156. }
  157. if (!$reflectionType = $reflectionParameter->getType()) {
  158. return null;
  159. }
  160. if (!$types = $this->extractFromReflectionType($reflectionType, $reflectionConstructor->getDeclaringClass())) {
  161. return null;
  162. }
  163. return $types;
  164. }
  165. private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
  166. {
  167. $reflectionParameter = null;
  168. foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
  169. if ($reflectionParameter->getName() === $property) {
  170. return $reflectionParameter;
  171. }
  172. }
  173. return null;
  174. }
  175. /**
  176. * {@inheritdoc}
  177. */
  178. public function isReadable(string $class, string $property, array $context = []): ?bool
  179. {
  180. if ($this->isAllowedProperty($class, $property)) {
  181. return true;
  182. }
  183. return null !== $this->getReadInfo($class, $property, $context);
  184. }
  185. /**
  186. * {@inheritdoc}
  187. */
  188. public function isWritable(string $class, string $property, array $context = []): ?bool
  189. {
  190. if ($this->isAllowedProperty($class, $property, true)) {
  191. return true;
  192. }
  193. [$reflectionMethod] = $this->getMutatorMethod($class, $property);
  194. return null !== $reflectionMethod;
  195. }
  196. /**
  197. * {@inheritdoc}
  198. */
  199. public function isInitializable(string $class, string $property, array $context = []): ?bool
  200. {
  201. try {
  202. $reflectionClass = new \ReflectionClass($class);
  203. } catch (\ReflectionException $e) {
  204. return null;
  205. }
  206. if (!$reflectionClass->isInstantiable()) {
  207. return false;
  208. }
  209. if ($constructor = $reflectionClass->getConstructor()) {
  210. foreach ($constructor->getParameters() as $parameter) {
  211. if ($property === $parameter->name) {
  212. return true;
  213. }
  214. }
  215. } elseif ($parentClass = $reflectionClass->getParentClass()) {
  216. return $this->isInitializable($parentClass->getName(), $property);
  217. }
  218. return false;
  219. }
  220. /**
  221. * {@inheritdoc}
  222. */
  223. public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo
  224. {
  225. try {
  226. $reflClass = new \ReflectionClass($class);
  227. } catch (\ReflectionException $e) {
  228. return null;
  229. }
  230. $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
  231. $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
  232. $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
  233. $allowMagicGet = (bool) ($magicMethods & self::ALLOW_MAGIC_GET);
  234. if (isset($context['enable_magic_call_extraction'])) {
  235. trigger_deprecation('symfony/property-info', '5.2', 'Using the "enable_magic_call_extraction" context option in "%s()" is deprecated. Use "enable_magic_methods_extraction" instead.', __METHOD__);
  236. $allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
  237. }
  238. $hasProperty = $reflClass->hasProperty($property);
  239. $camelProp = $this->camelize($property);
  240. $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item)
  241. foreach ($this->accessorPrefixes as $prefix) {
  242. $methodName = $prefix.$camelProp;
  243. if ($reflClass->hasMethod($methodName) && $reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags && !$reflClass->getMethod($methodName)->getNumberOfRequiredParameters()) {
  244. $method = $reflClass->getMethod($methodName);
  245. return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisiblityForMethod($method), $method->isStatic(), false);
  246. }
  247. }
  248. if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) {
  249. $method = $reflClass->getMethod($getsetter);
  250. return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisiblityForMethod($method), $method->isStatic(), false);
  251. }
  252. if ($allowMagicGet && $reflClass->hasMethod('__get') && ($reflClass->getMethod('__get')->getModifiers() & $this->methodReflectionFlags)) {
  253. return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, PropertyReadInfo::VISIBILITY_PUBLIC, false, false);
  254. }
  255. if ($hasProperty && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) {
  256. $reflProperty = $reflClass->getProperty($property);
  257. return new PropertyReadInfo(PropertyReadInfo::TYPE_PROPERTY, $property, $this->getReadVisiblityForProperty($reflProperty), $reflProperty->isStatic(), true);
  258. }
  259. if ($allowMagicCall && $reflClass->hasMethod('__call') && ($reflClass->getMethod('__call')->getModifiers() & $this->methodReflectionFlags)) {
  260. return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, 'get'.$camelProp, PropertyReadInfo::VISIBILITY_PUBLIC, false, false);
  261. }
  262. return null;
  263. }
  264. /**
  265. * {@inheritdoc}
  266. */
  267. public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo
  268. {
  269. try {
  270. $reflClass = new \ReflectionClass($class);
  271. } catch (\ReflectionException $e) {
  272. return null;
  273. }
  274. $allowGetterSetter = $context['enable_getter_setter_extraction'] ?? false;
  275. $magicMethods = $context['enable_magic_methods_extraction'] ?? $this->magicMethodsFlags;
  276. $allowMagicCall = (bool) ($magicMethods & self::ALLOW_MAGIC_CALL);
  277. $allowMagicSet = (bool) ($magicMethods & self::ALLOW_MAGIC_SET);
  278. if (isset($context['enable_magic_call_extraction'])) {
  279. trigger_deprecation('symfony/property-info', '5.2', 'Using the "enable_magic_call_extraction" context option in "%s()" is deprecated. Use "enable_magic_methods_extraction" instead.', __METHOD__);
  280. $allowMagicCall = $context['enable_magic_call_extraction'] ?? false;
  281. }
  282. $allowConstruct = $context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction;
  283. $allowAdderRemover = $context['enable_adder_remover_extraction'] ?? true;
  284. $camelized = $this->camelize($property);
  285. $constructor = $reflClass->getConstructor();
  286. $singulars = $this->inflector->singularize($camelized);
  287. $errors = [];
  288. if (null !== $constructor && $allowConstruct) {
  289. foreach ($constructor->getParameters() as $parameter) {
  290. if ($parameter->getName() === $property) {
  291. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_CONSTRUCTOR, $property);
  292. }
  293. }
  294. }
  295. [$adderAccessName, $removerAccessName, $adderAndRemoverErrors] = $this->findAdderAndRemover($reflClass, $singulars);
  296. if ($allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) {
  297. $adderMethod = $reflClass->getMethod($adderAccessName);
  298. $removerMethod = $reflClass->getMethod($removerAccessName);
  299. $mutator = new PropertyWriteInfo(PropertyWriteInfo::TYPE_ADDER_AND_REMOVER);
  300. $mutator->setAdderInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $adderAccessName, $this->getWriteVisiblityForMethod($adderMethod), $adderMethod->isStatic()));
  301. $mutator->setRemoverInfo(new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $removerAccessName, $this->getWriteVisiblityForMethod($removerMethod), $removerMethod->isStatic()));
  302. return $mutator;
  303. }
  304. $errors[] = $adderAndRemoverErrors;
  305. foreach ($this->mutatorPrefixes as $mutatorPrefix) {
  306. $methodName = $mutatorPrefix.$camelized;
  307. [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $methodName, 1);
  308. if (!$accessible) {
  309. $errors[] = $methodAccessibleErrors;
  310. continue;
  311. }
  312. $method = $reflClass->getMethod($methodName);
  313. if (!\in_array($mutatorPrefix, $this->arrayMutatorPrefixes, true)) {
  314. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $methodName, $this->getWriteVisiblityForMethod($method), $method->isStatic());
  315. }
  316. }
  317. $getsetter = lcfirst($camelized);
  318. if ($allowGetterSetter) {
  319. [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, $getsetter, 1);
  320. if ($accessible) {
  321. $method = $reflClass->getMethod($getsetter);
  322. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, $getsetter, $this->getWriteVisiblityForMethod($method), $method->isStatic());
  323. }
  324. $errors[] = $methodAccessibleErrors;
  325. }
  326. if ($reflClass->hasProperty($property) && ($reflClass->getProperty($property)->getModifiers() & $this->propertyReflectionFlags)) {
  327. $reflProperty = $reflClass->getProperty($property);
  328. if (\PHP_VERSION_ID < 80100 || !$reflProperty->isReadOnly()) {
  329. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, $this->getWriteVisiblityForProperty($reflProperty), $reflProperty->isStatic());
  330. }
  331. $errors[] = [sprintf('The property "%s" in class "%s" is a promoted readonly property.', $property, $reflClass->getName())];
  332. $allowMagicSet = $allowMagicCall = false;
  333. }
  334. if ($allowMagicSet) {
  335. [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__set', 2);
  336. if ($accessible) {
  337. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_PROPERTY, $property, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
  338. }
  339. $errors[] = $methodAccessibleErrors;
  340. }
  341. if ($allowMagicCall) {
  342. [$accessible, $methodAccessibleErrors] = $this->isMethodAccessible($reflClass, '__call', 2);
  343. if ($accessible) {
  344. return new PropertyWriteInfo(PropertyWriteInfo::TYPE_METHOD, 'set'.$camelized, PropertyWriteInfo::VISIBILITY_PUBLIC, false);
  345. }
  346. $errors[] = $methodAccessibleErrors;
  347. }
  348. if (!$allowAdderRemover && null !== $adderAccessName && null !== $removerAccessName) {
  349. $errors[] = [sprintf(
  350. 'The property "%s" in class "%s" can be defined with the methods "%s()" but '.
  351. 'the new value must be an array or an instance of \Traversable',
  352. $property,
  353. $reflClass->getName(),
  354. implode('()", "', [$adderAccessName, $removerAccessName])
  355. )];
  356. }
  357. $noneProperty = new PropertyWriteInfo();
  358. $noneProperty->setErrors(array_merge([], ...$errors));
  359. return $noneProperty;
  360. }
  361. /**
  362. * @return Type[]|null
  363. */
  364. private function extractFromMutator(string $class, string $property): ?array
  365. {
  366. [$reflectionMethod, $prefix] = $this->getMutatorMethod($class, $property);
  367. if (null === $reflectionMethod) {
  368. return null;
  369. }
  370. $reflectionParameters = $reflectionMethod->getParameters();
  371. $reflectionParameter = $reflectionParameters[0];
  372. if (!$reflectionType = $reflectionParameter->getType()) {
  373. return null;
  374. }
  375. $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
  376. if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) {
  377. $type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])];
  378. }
  379. return $type;
  380. }
  381. /**
  382. * Tries to extract type information from accessors.
  383. *
  384. * @return Type[]|null
  385. */
  386. private function extractFromAccessor(string $class, string $property): ?array
  387. {
  388. [$reflectionMethod, $prefix] = $this->getAccessorMethod($class, $property);
  389. if (null === $reflectionMethod) {
  390. return null;
  391. }
  392. if ($reflectionType = $reflectionMethod->getReturnType()) {
  393. return $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass());
  394. }
  395. if (\in_array($prefix, ['is', 'can', 'has'])) {
  396. return [new Type(Type::BUILTIN_TYPE_BOOL)];
  397. }
  398. return null;
  399. }
  400. /**
  401. * Tries to extract type information from constructor.
  402. *
  403. * @return Type[]|null
  404. */
  405. private function extractFromConstructor(string $class, string $property): ?array
  406. {
  407. try {
  408. $reflectionClass = new \ReflectionClass($class);
  409. } catch (\ReflectionException $e) {
  410. return null;
  411. }
  412. $constructor = $reflectionClass->getConstructor();
  413. if (!$constructor) {
  414. return null;
  415. }
  416. foreach ($constructor->getParameters() as $parameter) {
  417. if ($property !== $parameter->name) {
  418. continue;
  419. }
  420. $reflectionType = $parameter->getType();
  421. return $reflectionType ? $this->extractFromReflectionType($reflectionType, $constructor->getDeclaringClass()) : null;
  422. }
  423. if ($parentClass = $reflectionClass->getParentClass()) {
  424. return $this->extractFromConstructor($parentClass->getName(), $property);
  425. }
  426. return null;
  427. }
  428. private function extractFromPropertyDeclaration(string $class, string $property): ?array
  429. {
  430. try {
  431. $reflectionClass = new \ReflectionClass($class);
  432. if (\PHP_VERSION_ID >= 70400) {
  433. $reflectionProperty = $reflectionClass->getProperty($property);
  434. $reflectionPropertyType = $reflectionProperty->getType();
  435. if (null !== $reflectionPropertyType && $types = $this->extractFromReflectionType($reflectionPropertyType, $reflectionProperty->getDeclaringClass())) {
  436. return $types;
  437. }
  438. }
  439. } catch (\ReflectionException $e) {
  440. return null;
  441. }
  442. $defaultValue = $reflectionClass->getDefaultProperties()[$property] ?? null;
  443. if (null === $defaultValue) {
  444. return null;
  445. }
  446. $type = \gettype($defaultValue);
  447. $type = static::MAP_TYPES[$type] ?? $type;
  448. return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)];
  449. }
  450. private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array
  451. {
  452. $types = [];
  453. $nullable = $reflectionType->allowsNull();
  454. foreach (($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) ? $reflectionType->getTypes() : [$reflectionType] as $type) {
  455. if (!$type instanceof \ReflectionNamedType) {
  456. // Nested composite types are not supported yet.
  457. return [];
  458. }
  459. $phpTypeOrClass = $type->getName();
  460. if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass || 'never' === $phpTypeOrClass) {
  461. continue;
  462. }
  463. if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
  464. $types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
  465. } elseif ('void' === $phpTypeOrClass) {
  466. $types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
  467. } elseif ($type->isBuiltin()) {
  468. $types[] = new Type($phpTypeOrClass, $nullable);
  469. } else {
  470. $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass));
  471. }
  472. }
  473. return $types;
  474. }
  475. private function resolveTypeName(string $name, \ReflectionClass $declaringClass): string
  476. {
  477. if ('self' === $lcName = strtolower($name)) {
  478. return $declaringClass->name;
  479. }
  480. if ('parent' === $lcName && $parent = $declaringClass->getParentClass()) {
  481. return $parent->name;
  482. }
  483. return $name;
  484. }
  485. private function isNullableProperty(string $class, string $property): bool
  486. {
  487. try {
  488. $reflectionProperty = new \ReflectionProperty($class, $property);
  489. if (\PHP_VERSION_ID >= 70400) {
  490. $reflectionPropertyType = $reflectionProperty->getType();
  491. return null !== $reflectionPropertyType && $reflectionPropertyType->allowsNull();
  492. }
  493. return false;
  494. } catch (\ReflectionException $e) {
  495. // Return false if the property doesn't exist
  496. }
  497. return false;
  498. }
  499. private function isAllowedProperty(string $class, string $property, bool $writeAccessRequired = false): bool
  500. {
  501. try {
  502. $reflectionProperty = new \ReflectionProperty($class, $property);
  503. if (\PHP_VERSION_ID >= 80100 && $writeAccessRequired && $reflectionProperty->isReadOnly()) {
  504. return false;
  505. }
  506. return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags);
  507. } catch (\ReflectionException $e) {
  508. // Return false if the property doesn't exist
  509. }
  510. return false;
  511. }
  512. /**
  513. * Gets the accessor method.
  514. *
  515. * Returns an array with a the instance of \ReflectionMethod as first key
  516. * and the prefix of the method as second or null if not found.
  517. */
  518. private function getAccessorMethod(string $class, string $property): ?array
  519. {
  520. $ucProperty = ucfirst($property);
  521. foreach ($this->accessorPrefixes as $prefix) {
  522. try {
  523. $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty);
  524. if ($reflectionMethod->isStatic()) {
  525. continue;
  526. }
  527. if (0 === $reflectionMethod->getNumberOfRequiredParameters()) {
  528. return [$reflectionMethod, $prefix];
  529. }
  530. } catch (\ReflectionException $e) {
  531. // Return null if the property doesn't exist
  532. }
  533. }
  534. return null;
  535. }
  536. /**
  537. * Returns an array with a the instance of \ReflectionMethod as first key
  538. * and the prefix of the method as second or null if not found.
  539. */
  540. private function getMutatorMethod(string $class, string $property): ?array
  541. {
  542. $ucProperty = ucfirst($property);
  543. $ucSingulars = $this->inflector->singularize($ucProperty);
  544. $mutatorPrefixes = \in_array($ucProperty, $ucSingulars, true) ? $this->arrayMutatorPrefixesLast : $this->arrayMutatorPrefixesFirst;
  545. foreach ($mutatorPrefixes as $prefix) {
  546. $names = [$ucProperty];
  547. if (\in_array($prefix, $this->arrayMutatorPrefixes)) {
  548. $names = array_merge($names, $ucSingulars);
  549. }
  550. foreach ($names as $name) {
  551. try {
  552. $reflectionMethod = new \ReflectionMethod($class, $prefix.$name);
  553. if ($reflectionMethod->isStatic()) {
  554. continue;
  555. }
  556. // Parameter can be optional to allow things like: method(?array $foo = null)
  557. if ($reflectionMethod->getNumberOfParameters() >= 1) {
  558. return [$reflectionMethod, $prefix];
  559. }
  560. } catch (\ReflectionException $e) {
  561. // Try the next prefix if the method doesn't exist
  562. }
  563. }
  564. }
  565. return null;
  566. }
  567. private function getPropertyName(string $methodName, array $reflectionProperties): ?string
  568. {
  569. $pattern = implode('|', array_merge($this->accessorPrefixes, $this->mutatorPrefixes));
  570. if ('' !== $pattern && preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) {
  571. if (!\in_array($matches[1], $this->arrayMutatorPrefixes)) {
  572. return $matches[2];
  573. }
  574. foreach ($reflectionProperties as $reflectionProperty) {
  575. foreach ($this->inflector->singularize($reflectionProperty->name) as $name) {
  576. if (strtolower($name) === strtolower($matches[2])) {
  577. return $reflectionProperty->name;
  578. }
  579. }
  580. }
  581. return $matches[2];
  582. }
  583. return null;
  584. }
  585. /**
  586. * Searches for add and remove methods.
  587. *
  588. * @param \ReflectionClass $reflClass The reflection class for the given object
  589. * @param array $singulars The singular form of the property name or null
  590. *
  591. * @return array An array containing the adder and remover when found and errors
  592. */
  593. private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars): array
  594. {
  595. if (!\is_array($this->arrayMutatorPrefixes) && 2 !== \count($this->arrayMutatorPrefixes)) {
  596. return [null, null, []];
  597. }
  598. [$addPrefix, $removePrefix] = $this->arrayMutatorPrefixes;
  599. $errors = [];
  600. foreach ($singulars as $singular) {
  601. $addMethod = $addPrefix.$singular;
  602. $removeMethod = $removePrefix.$singular;
  603. [$addMethodFound, $addMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $addMethod, 1);
  604. [$removeMethodFound, $removeMethodAccessibleErrors] = $this->isMethodAccessible($reflClass, $removeMethod, 1);
  605. $errors[] = $addMethodAccessibleErrors;
  606. $errors[] = $removeMethodAccessibleErrors;
  607. if ($addMethodFound && $removeMethodFound) {
  608. return [$addMethod, $removeMethod, []];
  609. }
  610. if ($addMethodFound && !$removeMethodFound) {
  611. $errors[] = [sprintf('The add method "%s" in class "%s" was found, but the corresponding remove method "%s" was not found', $addMethod, $reflClass->getName(), $removeMethod)];
  612. } elseif (!$addMethodFound && $removeMethodFound) {
  613. $errors[] = [sprintf('The remove method "%s" in class "%s" was found, but the corresponding add method "%s" was not found', $removeMethod, $reflClass->getName(), $addMethod)];
  614. }
  615. }
  616. return [null, null, array_merge([], ...$errors)];
  617. }
  618. /**
  619. * Returns whether a method is public and has the number of required parameters and errors.
  620. */
  621. private function isMethodAccessible(\ReflectionClass $class, string $methodName, int $parameters): array
  622. {
  623. $errors = [];
  624. if ($class->hasMethod($methodName)) {
  625. $method = $class->getMethod($methodName);
  626. if (\ReflectionMethod::IS_PUBLIC === $this->methodReflectionFlags && !$method->isPublic()) {
  627. $errors[] = sprintf('The method "%s" in class "%s" was found but does not have public access.', $methodName, $class->getName());
  628. } elseif ($method->getNumberOfRequiredParameters() > $parameters || $method->getNumberOfParameters() < $parameters) {
  629. $errors[] = sprintf('The method "%s" in class "%s" requires %d arguments, but should accept only %d.', $methodName, $class->getName(), $method->getNumberOfRequiredParameters(), $parameters);
  630. } else {
  631. return [true, $errors];
  632. }
  633. }
  634. return [false, $errors];
  635. }
  636. /**
  637. * Camelizes a given string.
  638. */
  639. private function camelize(string $string): string
  640. {
  641. return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
  642. }
  643. /**
  644. * Return allowed reflection method flags.
  645. */
  646. private function getMethodsFlags(int $accessFlags): int
  647. {
  648. $methodFlags = 0;
  649. if ($accessFlags & self::ALLOW_PUBLIC) {
  650. $methodFlags |= \ReflectionMethod::IS_PUBLIC;
  651. }
  652. if ($accessFlags & self::ALLOW_PRIVATE) {
  653. $methodFlags |= \ReflectionMethod::IS_PRIVATE;
  654. }
  655. if ($accessFlags & self::ALLOW_PROTECTED) {
  656. $methodFlags |= \ReflectionMethod::IS_PROTECTED;
  657. }
  658. return $methodFlags;
  659. }
  660. /**
  661. * Return allowed reflection property flags.
  662. */
  663. private function getPropertyFlags(int $accessFlags): int
  664. {
  665. $propertyFlags = 0;
  666. if ($accessFlags & self::ALLOW_PUBLIC) {
  667. $propertyFlags |= \ReflectionProperty::IS_PUBLIC;
  668. }
  669. if ($accessFlags & self::ALLOW_PRIVATE) {
  670. $propertyFlags |= \ReflectionProperty::IS_PRIVATE;
  671. }
  672. if ($accessFlags & self::ALLOW_PROTECTED) {
  673. $propertyFlags |= \ReflectionProperty::IS_PROTECTED;
  674. }
  675. return $propertyFlags;
  676. }
  677. private function getReadVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
  678. {
  679. if ($reflectionProperty->isPrivate()) {
  680. return PropertyReadInfo::VISIBILITY_PRIVATE;
  681. }
  682. if ($reflectionProperty->isProtected()) {
  683. return PropertyReadInfo::VISIBILITY_PROTECTED;
  684. }
  685. return PropertyReadInfo::VISIBILITY_PUBLIC;
  686. }
  687. private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
  688. {
  689. if ($reflectionMethod->isPrivate()) {
  690. return PropertyReadInfo::VISIBILITY_PRIVATE;
  691. }
  692. if ($reflectionMethod->isProtected()) {
  693. return PropertyReadInfo::VISIBILITY_PROTECTED;
  694. }
  695. return PropertyReadInfo::VISIBILITY_PUBLIC;
  696. }
  697. private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
  698. {
  699. if ($reflectionProperty->isPrivate()) {
  700. return PropertyWriteInfo::VISIBILITY_PRIVATE;
  701. }
  702. if ($reflectionProperty->isProtected()) {
  703. return PropertyWriteInfo::VISIBILITY_PROTECTED;
  704. }
  705. return PropertyWriteInfo::VISIBILITY_PUBLIC;
  706. }
  707. private function getWriteVisiblityForMethod(\ReflectionMethod $reflectionMethod): string
  708. {
  709. if ($reflectionMethod->isPrivate()) {
  710. return PropertyWriteInfo::VISIBILITY_PRIVATE;
  711. }
  712. if ($reflectionMethod->isProtected()) {
  713. return PropertyWriteInfo::VISIBILITY_PROTECTED;
  714. }
  715. return PropertyWriteInfo::VISIBILITY_PUBLIC;
  716. }
  717. }