ReflectionExtractor.php 28 KB

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