1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection\Php;
4:
5: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty;
6: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
7: use PHPStan\Reflection\AttributeReflection;
8: use PHPStan\Reflection\ClassReflection;
9: use PHPStan\Reflection\ExtendedMethodReflection;
10: use PHPStan\Reflection\ExtendedPropertyReflection;
11: use PHPStan\Reflection\MissingMethodFromReflectionException;
12: use PHPStan\TrinaryLogic;
13: use PHPStan\Turbo\ShadowedByTurboExtension;
14: use PHPStan\Type\MixedType;
15: use PHPStan\Type\NeverType;
16: use PHPStan\Type\Type;
17: use PHPStan\Type\TypehintHelper;
18: use function sprintf;
19:
20: /**
21: * @api
22: */
23: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/PhpPropertyReflection.cpp')]
24: final class PhpPropertyReflection implements ExtendedPropertyReflection
25: {
26:
27: private ?Type $readableType = null;
28:
29: private ?Type $writableType = null;
30:
31: /**
32: * @param list<AttributeReflection> $attributes
33: */
34: public function __construct(
35: private ClassReflection $declaringClass,
36: private ?ClassReflection $declaringTrait,
37: private Type $nativeType,
38: private ?Type $readablePhpDocType,
39: private ?Type $writablePhpDocType,
40: private ReflectionProperty $reflection,
41: private ?ExtendedMethodReflection $getHook,
42: private ?ExtendedMethodReflection $setHook,
43: private ?ResolvedPhpDocBlock $resolvedPhpDocBlock,
44: private ?string $deprecatedDescription,
45: private bool $isDeprecated,
46: private bool $isInternal,
47: private bool $isReadOnlyByPhpDoc,
48: private bool $isAllowedPrivateMutation,
49: private array $attributes,
50: private bool $isFinal,
51: private bool $readable,
52: private bool $writable,
53: private bool $private,
54: private bool $public,
55: )
56: {
57: }
58:
59: public function getName(): string
60: {
61: return $this->reflection->getName();
62: }
63:
64: public function getDeclaringClass(): ClassReflection
65: {
66: return $this->declaringClass;
67: }
68:
69: public function getDeclaringTrait(): ?ClassReflection
70: {
71: return $this->declaringTrait;
72: }
73:
74: public function getDocComment(): ?string
75: {
76: $docComment = $this->reflection->getDocComment();
77: if ($docComment === false) {
78: return null;
79: }
80:
81: return $docComment;
82: }
83:
84: public function isStatic(): bool
85: {
86: return $this->reflection->isStatic();
87: }
88:
89: public function isPrivate(): bool
90: {
91: return $this->private;
92: }
93:
94: public function isPublic(): bool
95: {
96: return $this->public;
97: }
98:
99: public function isReadOnly(): bool
100: {
101: return $this->reflection->isReadOnly();
102: }
103:
104: public function isReadOnlyByPhpDoc(): bool
105: {
106: return $this->isReadOnlyByPhpDoc;
107: }
108:
109: public function getReadableType(): Type
110: {
111: return $this->readableType ??= TypehintHelper::decideType(
112: $this->nativeType,
113: $this->readablePhpDocType,
114: );
115: }
116:
117: public function getWritableType(): Type
118: {
119: if ($this->hasHook('set')) {
120: $setHookVariant = $this->getHook('set')->getOnlyVariant();
121: $parameters = $setHookVariant->getParameters();
122: if (isset($parameters[0])) {
123: return $parameters[0]->getType();
124: }
125: }
126:
127: if ($this->writableType !== null) {
128: return $this->writableType;
129: }
130:
131: if ($this->writablePhpDocType === null || $this->writablePhpDocType instanceof NeverType) {
132: return $this->writableType = TypehintHelper::decideType(
133: $this->nativeType,
134: $this->readablePhpDocType,
135: );
136: }
137:
138: if (
139: $this->readablePhpDocType !== null
140: && !$this->readablePhpDocType->equals($this->writablePhpDocType)
141: ) {
142: return $this->writableType = $this->writablePhpDocType;
143: }
144:
145: return $this->writableType = TypehintHelper::decideType(
146: $this->nativeType,
147: $this->writablePhpDocType,
148: );
149: }
150:
151: public function canChangeTypeAfterAssignment(): bool
152: {
153: if ($this->isStatic()) {
154: return $this->getReadableType()->equals($this->getWritableType());
155: }
156:
157: if ($this->isVirtual()->yes()) {
158: return false;
159: }
160:
161: if ($this->hasHook('get')) {
162: return false;
163: }
164:
165: if ($this->hasHook('set')) {
166: return false;
167: }
168:
169: return $this->getReadableType()->equals($this->getWritableType());
170: }
171:
172: public function isPromoted(): bool
173: {
174: return $this->reflection->isPromoted();
175: }
176:
177: public function hasPhpDocType(): bool
178: {
179: return $this->readablePhpDocType !== null;
180: }
181:
182: public function getPhpDocType(): Type
183: {
184: if ($this->readablePhpDocType !== null) {
185: return $this->readablePhpDocType;
186: }
187:
188: return new MixedType();
189: }
190:
191: public function hasNativeType(): bool
192: {
193: return !$this->nativeType instanceof MixedType || $this->nativeType->isExplicitMixed();
194: }
195:
196: public function getNativeType(): Type
197: {
198: return $this->nativeType;
199: }
200:
201: public function isReadable(): bool
202: {
203: if (!$this->readable) {
204: return false;
205: }
206:
207: if ($this->isStatic()) {
208: return true;
209: }
210:
211: if (!$this->isVirtual()->yes()) {
212: return true;
213: }
214:
215: return $this->hasHook('get');
216: }
217:
218: public function isWritable(): bool
219: {
220: if (!$this->writable) {
221: return false;
222: }
223:
224: if ($this->isStatic()) {
225: return true;
226: }
227:
228: if (!$this->isVirtual()->yes()) {
229: return true;
230: }
231:
232: return $this->hasHook('set');
233: }
234:
235: public function getDeprecatedDescription(): ?string
236: {
237: if ($this->isDeprecated) {
238: return $this->deprecatedDescription;
239: }
240:
241: return null;
242: }
243:
244: public function isDeprecated(): TrinaryLogic
245: {
246: return TrinaryLogic::createFromBoolean($this->isDeprecated);
247: }
248:
249: public function isInternal(): TrinaryLogic
250: {
251: return TrinaryLogic::createFromBoolean($this->isInternal);
252: }
253:
254: public function isAllowedPrivateMutation(): bool
255: {
256: return $this->isAllowedPrivateMutation;
257: }
258:
259: public function getNativeReflection(): ReflectionProperty
260: {
261: return $this->reflection;
262: }
263:
264: public function isAbstract(): TrinaryLogic
265: {
266: return TrinaryLogic::createFromBoolean($this->reflection->isAbstract());
267: }
268:
269: public function isFinalByKeyword(): TrinaryLogic
270: {
271: return TrinaryLogic::createFromBoolean($this->reflection->isFinal());
272: }
273:
274: public function isFinal(): TrinaryLogic
275: {
276: return TrinaryLogic::createFromBoolean($this->isFinal);
277: }
278:
279: public function isVirtual(): TrinaryLogic
280: {
281: return TrinaryLogic::createFromBoolean($this->reflection->isVirtual());
282: }
283:
284: public function hasHook(string $hookType): bool
285: {
286: if ($hookType === 'get') {
287: return $this->getHook !== null;
288: }
289:
290: return $this->setHook !== null;
291: }
292:
293: public function isHooked(): bool
294: {
295: return $this->getHook !== null || $this->setHook !== null;
296: }
297:
298: public function getHook(string $hookType): ExtendedMethodReflection
299: {
300: if ($hookType === 'get') {
301: if ($this->getHook === null) {
302: throw new MissingMethodFromReflectionException($this->declaringClass->getName(), sprintf('$%s::get', $this->reflection->getName()));
303: }
304:
305: return $this->getHook;
306: }
307:
308: if ($this->setHook === null) {
309: throw new MissingMethodFromReflectionException($this->declaringClass->getName(), sprintf('$%s::set', $this->reflection->getName()));
310: }
311:
312: return $this->setHook;
313: }
314:
315: public function isProtectedSet(): bool
316: {
317: return $this->reflection->isProtectedSet();
318: }
319:
320: public function isPrivateSet(): bool
321: {
322: return $this->reflection->isPrivateSet();
323: }
324:
325: public function getAttributes(): array
326: {
327: return $this->attributes;
328: }
329:
330: public function isDummy(): TrinaryLogic
331: {
332: return TrinaryLogic::createNo();
333: }
334:
335: public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
336: {
337: return $this->resolvedPhpDocBlock;
338: }
339:
340: }
341: