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