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