1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use Closure;
8: use Error;
9: use OutOfBoundsException;
10: use PhpParser\Node;
11: use PhpParser\Node\Stmt\Property as PropertyNode;
12: use PhpParser\NodeTraverser;
13: use PhpParser\NodeVisitor\FindingVisitor;
14: use ReflectionClass as CoreReflectionClass;
15: use ReflectionException;
16: use ReflectionProperty as CoreReflectionProperty;
17: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
18: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
19: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
20: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
21: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
22: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
23: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
24: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
25: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
26: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
27: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
28: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionPropertyStringCast;
29: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
30: use PHPStan\BetterReflection\Reflector\Reflector;
31: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
32: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
33: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
34: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
35: use PHPStan\BetterReflection\Util\GetLastDocComment;
36:
37: use function array_map;
38: use function assert;
39: use function func_num_args;
40: use function is_object;
41: use function sprintf;
42: use function str_contains;
43:
44: /** @psalm-immutable */
45: class ReflectionProperty
46: {
47: private Reflector $reflector;
48: private bool $isPromoted;
49: private bool $declaredAtCompileTime;
50: /** @var non-empty-string */
51: private string $name;
52:
53: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> */
54: private int $modifiers;
55:
56: /**
57: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
58: */
59: private $type;
60:
61: /**
62: * @var \PhpParser\Node\Expr|null
63: */
64: private $default;
65:
66: /** @var non-empty-string|null */
67: private $docComment;
68:
69: /** @var list<ReflectionAttribute> */
70: private array $attributes;
71:
72: /** @var positive-int|null */
73: private $startLine;
74:
75: /** @var positive-int|null */
76: private $endLine;
77:
78: /** @var positive-int|null */
79: private $startColumn;
80:
81: /** @var positive-int|null */
82: private $endColumn;
83:
84: private ?ReflectionClass $declaringClass;
85:
86: private ?ReflectionClass $implementingClass;
87:
88: /** @var non-empty-string */
89: private string $declaringClassName;
90:
91: /** @var non-empty-string */
92: private string $implementingClassName;
93:
94: private bool $immediateVirtual;
95:
96: /** @var array{get?: ReflectionMethod, set?: ReflectionMethod} */
97: private array $immediateHooks;
98:
99: /**
100: * @var array{get?: ReflectionMethod, set?: ReflectionMethod}|null
101: * @psalm-allow-private-mutation
102: */
103: private $cachedHooks = null;
104:
105: /** @psalm-allow-private-mutation
106: * @var bool|null */
107: private $cachedVirtual = null;
108:
109: /** @psalm-allow-private-mutation
110: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
111: private $compiledDefaultValue = null;
112:
113: private function __construct(Reflector $reflector, PropertyNode $node, Node\PropertyItem $propertyNode, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted, bool $declaredAtCompileTime)
114: {
115: $this->reflector = $reflector;
116: $this->isPromoted = $isPromoted;
117: $this->declaredAtCompileTime = $declaredAtCompileTime;
118: $this->declaringClass = $declaringClass;
119: $this->implementingClass = $implementingClass;
120: $this->name = $propertyNode->name->name;
121: $this->modifiers = $this->computeModifiers($node);
122: $this->type = $this->createType($node);
123: $this->default = $propertyNode->default;
124: $this->docComment = GetLastDocComment::forNode($node);
125: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
126: $this->immediateVirtual = $this->computeImmediateVirtual($node);
127: $this->immediateHooks = $this->createImmediateHooks($node);
128: $startLine = $node->getStartLine();
129: if ($startLine === -1) {
130: $startLine = null;
131: }
132: $endLine = $node->getEndLine();
133: if ($endLine === -1) {
134: $endLine = null;
135: }
136: /** @psalm-suppress InvalidPropertyAssignmentValue */
137: $this->startLine = $startLine;
138: /** @psalm-suppress InvalidPropertyAssignmentValue */
139: $this->endLine = $endLine;
140: try {
141: $this->startColumn = CalculateReflectionColumn::getStartColumn($declaringClass->getLocatedSource()->getSource(), $node);
142: } catch (NoNodePosition $exception) {
143: $this->startColumn = null;
144: }
145: try {
146: $this->endColumn = CalculateReflectionColumn::getEndColumn($declaringClass->getLocatedSource()->getSource(), $node);
147: } catch (NoNodePosition $exception) {
148: $this->endColumn = null;
149: }
150: $this->declaringClassName = $this->declaringClass->getName();
151: $this->implementingClassName = $this->implementingClass->getName();
152: }
153:
154: /**
155: * @return array<string, mixed>
156: */
157: public function exportToCache(): array
158: {
159: return [
160: 'declaringClassName' => $this->declaringClassName,
161: 'implementingClassName' => $this->implementingClassName,
162: 'name' => $this->name,
163: 'modifiers' => $this->modifiers,
164: 'type' => $this->type !== null ? ['class' => get_class($this->type), 'data' => $this->type->exportToCache()] : null,
165: 'default' => $this->default !== null ? ExprCacheHelper::export($this->default) : null,
166: 'docComment' => $this->docComment,
167: 'attributes' => array_map(
168: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
169: $this->attributes,
170: ),
171: 'startLine' => $this->startLine,
172: 'endLine' => $this->endLine,
173: 'startColumn' => $this->startColumn,
174: 'endColumn' => $this->endColumn,
175: 'isPromoted' => $this->isPromoted,
176: 'declaredAtCompileTime' => $this->declaredAtCompileTime,
177: 'immediateVirtual' => $this->immediateVirtual,
178: 'immediateHooks' => array_map(
179: static fn (ReflectionMethod $method) => $method->exportToCache(),
180: $this->immediateHooks,
181: ),
182: ];
183: }
184:
185: /**
186: * @param array<string, mixed> $data
187: */
188: public static function importFromCache(Reflector $reflector, array $data, LocatedSource $locatedSource): self
189: {
190: $reflection = new CoreReflectionClass(self::class);
191: /** @var self $ref */
192: $ref = $reflection->newInstanceWithoutConstructor();
193: $ref->reflector = $reflector;
194: $ref->declaringClassName = $data['declaringClassName'];
195: $ref->implementingClassName = $data['implementingClassName'];
196: $ref->name = $data['name'];
197: $ref->modifiers = $data['modifiers'];
198:
199: if ($data['type'] !== null) {
200: $typeClass = $data['type']['class'];
201: $ref->type = $typeClass::importFromCache($reflector, $data['type']['data'], $ref);
202: } else {
203: $ref->type = null;
204: }
205:
206: if ($data['default'] !== null) {
207: $ref->default = ExprCacheHelper::import($data['default']);
208: } else {
209: $ref->default = null;
210: }
211:
212: $ref->docComment = $data['docComment'];
213: $ref->attributes = array_map(
214: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
215: $data['attributes'],
216: );
217: $ref->startLine = $data['startLine'];
218: $ref->endLine = $data['endLine'];
219: $ref->startColumn = $data['startColumn'];
220: $ref->endColumn = $data['endColumn'];
221: $ref->isPromoted = $data['isPromoted'];
222: $ref->declaredAtCompileTime = $data['declaredAtCompileTime'];
223: $ref->immediateVirtual = $data['immediateVirtual'];
224: $ref->immediateHooks = array_map(
225: static fn ($hookData) => ReflectionMethod::importFromCache($reflector, $hookData, $locatedSource, $ref),
226: $data['immediateHooks'],
227: );
228:
229: return $ref;
230: }
231:
232: /**
233: * Create a reflection of an instance's property by its name
234: *
235: * @param non-empty-string $propertyName
236: *
237: * @throws ReflectionException
238: * @throws IdentifierNotFound
239: * @throws OutOfBoundsException
240: */
241: public static function createFromInstance(object $instance, string $propertyName): self
242: {
243: $property = ReflectionClass::createFromInstance($instance)->getProperty($propertyName);
244:
245: if ($property === null) {
246: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
247: }
248:
249: return $property;
250: }
251:
252: /** @internal */
253: public function withImplementingClass(ReflectionClass $implementingClass): self
254: {
255: $clone = clone $this;
256: $clone->implementingClass = $implementingClass;
257:
258: if ($clone->type !== null) {
259: $clone->type = $clone->type->withOwner($clone);
260: }
261:
262: $clone->attributes = array_map(static fn (ReflectionAttribute $attribute): ReflectionAttribute => $attribute->withOwner($clone), $this->attributes);
263:
264: $this->compiledDefaultValue = null;
265:
266: return $clone;
267: }
268:
269: /** @return non-empty-string */
270: public function __toString(): string
271: {
272: return ReflectionPropertyStringCast::toString($this);
273: }
274:
275: /**
276: * @internal
277: *
278: * @param PropertyNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
279: */
280: public static function createFromNode(Reflector $reflector, PropertyNode $node, Node\PropertyItem $propertyProperty, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted = false, bool $declaredAtCompileTime = true): self
281: {
282: return new self(
283: $reflector,
284: $node,
285: $propertyProperty,
286: $declaringClass,
287: $implementingClass,
288: $isPromoted,
289: $declaredAtCompileTime,
290: );
291: }
292:
293: /**
294: * Has the property been declared at compile-time?
295: *
296: * Note that unless the property is static, this is hard coded to return
297: * true, because we are unable to reflect instances of classes, therefore
298: * we can be sure that all properties are always declared at compile-time.
299: */
300: public function isDefault(): bool
301: {
302: return $this->declaredAtCompileTime;
303: }
304:
305: public function isDynamic(): bool
306: {
307: return ! $this->isDefault();
308: }
309:
310: /**
311: * Get the core-reflection-compatible modifier values.
312: *
313: * @return int-mask-of<ReflectionPropertyAdapter::IS_*>
314: */
315: public function getModifiers(): int
316: {
317: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> $modifiers */
318: $modifiers = $this->modifiers
319: + ($this->isVirtual() ? ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY : 0);
320:
321: return $modifiers;
322: }
323:
324: /**
325: * Get the name of the property.
326: *
327: * @return non-empty-string
328: */
329: public function getName(): string
330: {
331: return $this->name;
332: }
333:
334: /**
335: * Is the property private?
336: */
337: public function isPrivate(): bool
338: {
339: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PRIVATE);
340: }
341:
342: public function isPrivateSet(): bool
343: {
344: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY);
345: }
346:
347: /**
348: * Is the property protected?
349: */
350: public function isProtected(): bool
351: {
352: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PROTECTED);
353: }
354:
355: public function isProtectedSet(): bool
356: {
357: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY);
358: }
359:
360: /**
361: * Is the property public?
362: */
363: public function isPublic(): bool
364: {
365: return (bool) ($this->modifiers & CoreReflectionProperty::IS_PUBLIC);
366: }
367:
368: /**
369: * Is the property static?
370: */
371: public function isStatic(): bool
372: {
373: return (bool) ($this->modifiers & CoreReflectionProperty::IS_STATIC);
374: }
375:
376: public function isFinal(): bool
377: {
378: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY);
379: }
380:
381: public function isAbstract(): bool
382: {
383: return (bool) ($this->modifiers & ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY)
384: || $this->getDeclaringClass()->isInterface();
385: }
386:
387: public function isPromoted(): bool
388: {
389: return $this->isPromoted;
390: }
391:
392: public function isInitialized(?object $object = null): bool
393: {
394: if ($object === null && $this->isStatic()) {
395: return ! $this->hasType() || $this->hasDefaultValue();
396: }
397:
398: try {
399: $this->getValue($object);
400:
401: return true;
402:
403: /** @phpstan-ignore catch.neverThrown */
404: } catch (Error $e) {
405: if (strpos($e->getMessage(), 'must not be accessed before initialization') !== false) {
406: return false;
407: }
408:
409: throw $e;
410: }
411: }
412:
413: public function isReadOnly(): bool
414: {
415: return ($this->modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
416: || $this->getDeclaringClass()->isReadOnly();
417: }
418:
419: public function getDeclaringClass(): ReflectionClass
420: {
421: return $this->declaringClass ??= $this->reflector->reflectClass($this->declaringClassName);
422: }
423:
424: public function getImplementingClass(): ReflectionClass
425: {
426: return $this->implementingClass ??= $this->reflector->reflectClass($this->implementingClassName);
427: }
428:
429: /** @return non-empty-string|null */
430: public function getDocComment(): ?string
431: {
432: return $this->docComment;
433: }
434:
435: public function hasDefaultValue(): bool
436: {
437: return ! $this->hasType() || $this->default !== null;
438: }
439:
440: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
441: {
442: return $this->default;
443: }
444:
445: /**
446: * Get the default value of the property (as defined before constructor is
447: * called, when the property is defined)
448: * @return mixed
449: */
450: public function getDefaultValue()
451: {
452: if ($this->default === null) {
453: return null;
454: }
455:
456: if ($this->compiledDefaultValue === null) {
457: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke(
458: $this->default,
459: new CompilerContext(
460: $this->reflector,
461: $this,
462: ),
463: );
464: }
465:
466: /** @psalm-var scalar|array<scalar>|null $value */
467: $value = $this->compiledDefaultValue->value;
468:
469: return $value;
470: }
471:
472: public function isDeprecated(): bool
473: {
474: return DeprecatedHelper::isDeprecated($this);
475: }
476:
477: /**
478: * Get the line number that this property starts on.
479: *
480: * @return positive-int
481: *
482: * @throws CodeLocationMissing
483: */
484: public function getStartLine(): int
485: {
486: if ($this->startLine === null) {
487: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
488: }
489:
490: return $this->startLine;
491: }
492:
493: /**
494: * Get the line number that this property ends on.
495: *
496: * @return positive-int
497: *
498: * @throws CodeLocationMissing
499: */
500: public function getEndLine(): int
501: {
502: if ($this->endLine === null) {
503: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
504: }
505:
506: return $this->endLine;
507: }
508:
509: /**
510: * @return positive-int
511: *
512: * @throws CodeLocationMissing
513: */
514: public function getStartColumn(): int
515: {
516: if ($this->startColumn === null) {
517: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
518: }
519:
520: return $this->startColumn;
521: }
522:
523: /**
524: * @return positive-int
525: *
526: * @throws CodeLocationMissing
527: */
528: public function getEndColumn(): int
529: {
530: if ($this->endColumn === null) {
531: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->getImplementingClass()->getName()));
532: }
533:
534: return $this->endColumn;
535: }
536:
537: /** @return list<ReflectionAttribute> */
538: public function getAttributes(): array
539: {
540: return $this->attributes;
541: }
542:
543: /** @return list<ReflectionAttribute> */
544: public function getAttributesByName(string $name): array
545: {
546: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
547: }
548:
549: /**
550: * @param class-string $className
551: *
552: * @return list<ReflectionAttribute>
553: */
554: public function getAttributesByInstance(string $className): array
555: {
556: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
557: }
558:
559: /**
560: * @throws ClassDoesNotExist
561: * @throws NoObjectProvided
562: * @throws ObjectNotInstanceOfClass
563: * @return mixed
564: */
565: public function getValue(?object $object = null)
566: {
567: $implementingClassName = $this->getImplementingClass()->getName();
568:
569: if ($this->isStatic()) {
570: $this->assertClassExist($implementingClassName);
571:
572: $closure = Closure::bind(fn (string $implementingClassName, string $propertyName) => $implementingClassName::${$propertyName}, null, $implementingClassName);
573:
574: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
575: assert($closure instanceof Closure);
576:
577: return $closure->__invoke($implementingClassName, $this->getName());
578: }
579:
580: $instance = $this->assertObject($object);
581:
582: $closure = Closure::bind(fn (object $instance, string $propertyName) => $instance->{$propertyName}, $instance, $implementingClassName);
583:
584: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
585: assert($closure instanceof Closure);
586:
587: return $closure->__invoke($instance, $this->getName());
588: }
589:
590: /**
591: * @throws ClassDoesNotExist
592: * @throws NoObjectProvided
593: * @throws NotAnObject
594: * @throws ObjectNotInstanceOfClass
595: * @param mixed $object
596: * @param mixed $value
597: */
598: public function setValue($object, $value = null): void
599: {
600: $implementingClassName = $this->getImplementingClass()->getName();
601:
602: if ($this->isStatic()) {
603: $this->assertClassExist($implementingClassName);
604:
605: $closure = Closure::bind(function (string $_implementingClassName, string $_propertyName, $value): void {
606: /** @psalm-suppress MixedAssignment */
607: $_implementingClassName::${$_propertyName} = $value;
608: }, null, $implementingClassName);
609:
610: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
611: assert($closure instanceof Closure);
612:
613: $closure->__invoke($implementingClassName, $this->getName(), func_num_args() === 2 ? $value : $object);
614:
615: return;
616: }
617:
618: $instance = $this->assertObject($object);
619:
620: $closure = Closure::bind(function (object $instance, string $propertyName, $value): void {
621: $instance->{$propertyName} = $value;
622: }, $instance, $implementingClassName);
623:
624: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
625: assert($closure instanceof Closure);
626:
627: $closure->__invoke($instance, $this->getName(), $value);
628: }
629:
630: /**
631: * Does this property allow null?
632: */
633: public function allowsNull(): bool
634: {
635: return $this->type === null || $this->type->allowsNull();
636: }
637:
638: /**
639: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
640: */
641: private function createType(PropertyNode $node)
642: {
643: $type = $node->type;
644:
645: if ($type === null) {
646: return null;
647: }
648:
649: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
650:
651: return ReflectionType::createFromNode($this->reflector, $this, $type);
652: }
653:
654: /**
655: * Get the ReflectionType instance representing the type declaration for
656: * this property
657: *
658: * (note: this has nothing to do with DocBlocks).
659: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
660: */
661: public function getType()
662: {
663: return $this->type;
664: }
665:
666: /**
667: * Does this property have a type declaration?
668: *
669: * (note: this has nothing to do with DocBlocks).
670: */
671: public function hasType(): bool
672: {
673: return $this->type !== null;
674: }
675:
676: public function isVirtual(): bool
677: {
678: $this->cachedVirtual ??= $this->createCachedVirtual();
679:
680: return $this->cachedVirtual;
681: }
682:
683: public function hasHooks(): bool
684: {
685: return $this->getHooks() !== [];
686: }
687:
688: /**
689: * @param ReflectionPropertyHookType::* $hookType
690: */
691: public function hasHook(string $hookType): bool
692: {
693: return isset($this->getHooks()[$hookType]);
694: }
695:
696: /**
697: * @param ReflectionPropertyHookType::* $hookType
698: */
699: public function getHook(string $hookType): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
700: {
701: return $this->getHooks()[$hookType] ?? null;
702: }
703:
704: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
705: public function getHooks(): array
706: {
707: $this->cachedHooks ??= $this->createCachedHooks();
708:
709: return $this->cachedHooks;
710: }
711:
712: /**
713: * @param class-string $className
714: *
715: * @throws ClassDoesNotExist
716: */
717: private function assertClassExist(string $className): void
718: {
719: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
720: throw new ClassDoesNotExist('Property cannot be retrieved as the class does not exist');
721: }
722: }
723:
724: /**
725: * @throws NoObjectProvided
726: * @throws NotAnObject
727: * @throws ObjectNotInstanceOfClass
728: *
729: * @psalm-assert object $object
730: * @param mixed $object
731: */
732: private function assertObject($object): object
733: {
734: if ($object === null) {
735: throw NoObjectProvided::create();
736: }
737:
738: if (! is_object($object)) {
739: throw NotAnObject::fromNonObject($object);
740: }
741:
742: $implementingClassName = $this->getImplementingClass()->getName();
743:
744: if (get_class($object) !== $implementingClassName) {
745: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
746: }
747:
748: return $object;
749: }
750:
751: /** @return int-mask-of<ReflectionPropertyAdapter::IS_*> */
752: private function computeModifiers(PropertyNode $node): int
753: {
754: $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY : 0;
755: $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0;
756: $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0;
757: $modifiers += ! $node->isPrivate() && $node->isPrivateSet() ? ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY : 0;
758: $modifiers += $node->isProtected() ? ReflectionPropertyAdapter::IS_PROTECTED : 0;
759: $modifiers += ! $node->isProtected() && $node->isProtectedSet() ? ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY : 0;
760: $modifiers += $node->isPublic() ? ReflectionPropertyAdapter::IS_PUBLIC : 0;
761: $modifiers += $node->isFinal() ? ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY : 0;
762: $modifiers += $node->isAbstract() ? ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY : 0;
763:
764: if (
765: ! ($modifiers & ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY)
766: && ($modifiers & ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY)
767: ) {
768: $modifiers += ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY;
769: }
770:
771: if (
772: ! ($modifiers & (ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY))
773: && ! $node->isPublicSet()
774: && $node->isPublic()
775: && ($modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY)
776: ) {
777: $modifiers += ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY;
778: }
779:
780: /** @phpstan-ignore return.type */
781: return $modifiers;
782: }
783:
784: private function computeImmediateVirtual(PropertyNode $node): bool
785: {
786: if ($node->hooks === []) {
787: return false;
788: }
789:
790: $setHook = null;
791: $getHook = null;
792:
793: foreach ($node->hooks as $hook) {
794: if ($hook->name->name === 'set') {
795: $setHook = $hook;
796: } elseif ($hook->name->name === 'get') {
797: $getHook = $hook;
798: }
799: }
800:
801: if ($setHook !== null && ! $this->computeImmediateVirtualBasedOnHook($setHook)) {
802: return false;
803: }
804:
805: if ($getHook === null) {
806: return true;
807: }
808:
809: return $this->computeImmediateVirtualBasedOnHook($getHook);
810: }
811:
812: private function computeImmediateVirtualBasedOnHook(Node\PropertyHook $hook): bool
813: {
814: $hookBody = $hook->getStmts();
815:
816: // Abstract property or property in interface
817: if ($hookBody === null) {
818: return true;
819: }
820:
821: $visitor = new FindingVisitor(static fn (Node $node): bool => $node instanceof Node\Expr\PropertyFetch);
822: $traverser = new NodeTraverser($visitor);
823: $traverser->traverse($hookBody);
824:
825: foreach ($visitor->getFoundNodes() as $propertyFetchNode) {
826: assert($propertyFetchNode instanceof Node\Expr\PropertyFetch);
827:
828: if (
829: $propertyFetchNode->var instanceof Node\Expr\Variable
830: && $propertyFetchNode->var->name === 'this'
831: && $propertyFetchNode->name instanceof Node\Identifier
832: && $propertyFetchNode->name->name === $this->name
833: ) {
834: return false;
835: }
836: }
837:
838: return true;
839: }
840:
841: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
842: private function createImmediateHooks(PropertyNode $node): array
843: {
844: $hooks = [];
845:
846: foreach ($node->hooks as $hook) {
847: $hookName = $hook->name->name;
848: assert($hookName === 'get' || $hookName === 'set');
849:
850: $hookType = $node->type;
851: assert($hookType === null || $hookType instanceof Node\Identifier || $hookType instanceof Node\Name || $hookType instanceof Node\NullableType || $hookType instanceof Node\UnionType || $hookType instanceof Node\IntersectionType);
852:
853: $hooks[$hookName] = ReflectionMethod::createFromPropertyHook(
854: $this->reflector,
855: $hook,
856: $this->getDeclaringClass()->getLocatedSource(),
857: sprintf('$%s::%s', $this->name, $hookName),
858: $hookType,
859: $this->getDeclaringClass(),
860: $this->getImplementingClass(),
861: $this->getDeclaringClass(),
862: $this,
863: );
864: }
865:
866: return $hooks;
867: }
868:
869: private function createCachedVirtual(): bool
870: {
871: if (! $this->immediateVirtual) {
872: return false;
873: }
874:
875: return (($nullsafeVariable1 = $this->getParentProperty()) ? $nullsafeVariable1->isVirtual() : null) ?? true;
876: }
877:
878: /** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
879: private function createCachedHooks(): array
880: {
881: $hooks = $this->immediateHooks;
882:
883: // Just optimization - we don't need to check parent property when both hooks are defined in this class
884: if (isset($hooks['get'], $hooks['set'])) {
885: return $hooks;
886: }
887:
888: $parentHooks = (($nullsafeVariable2 = $this->getParentProperty()) ? $nullsafeVariable2->getHooks() : null) ?? [];
889:
890: foreach ($parentHooks as $hookName => $parentHook) {
891: if (isset($hooks[$hookName])) {
892: continue;
893: }
894:
895: $hooks[$hookName] = $parentHook;
896: }
897:
898: return $hooks;
899: }
900:
901: private function getParentProperty(): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
902: {
903: return ($nullsafeVariable3 = $this->getDeclaringClass()->getParentClass()) ? $nullsafeVariable3->getProperty($this->name) : null;
904: }
905: }
906: