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