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