ReflectionExtractor.php 38 KB

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