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