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 ReflectionException;
13: use ReflectionProperty as CoreReflectionProperty;
14: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
15: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
16: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
17: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
18: use PHPStan\BetterReflection\Reflection\Annotation\AnnotationHelper;
19: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
20: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
21: use PHPStan\BetterReflection\Reflection\Exception\CodeLocationMissing;
22: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
23: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
24: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
25: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionPropertyStringCast;
26: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
27: use PHPStan\BetterReflection\Reflector\Reflector;
28: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
29: use PHPStan\BetterReflection\Util\ClassExistenceChecker;
30: use PHPStan\BetterReflection\Util\Exception\NoNodePosition;
31: use PHPStan\BetterReflection\Util\GetLastDocComment;
32:
33: use function array_map;
34: use function assert;
35: use function func_num_args;
36: use function is_object;
37: use function sprintf;
38: use function str_contains;
39:
40: /** @psalm-immutable */
41: class ReflectionProperty
42: {
43: /** @var non-empty-string */
44: private $name;
45:
46: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> */
47: private $modifiers;
48:
49: /**
50: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
51: */
52: private $type;
53:
54: /**
55: * @var \PhpParser\Node\Expr|null
56: */
57: private $default;
58:
59: /** @var non-empty-string|null */
60: private $docComment;
61:
62: /** @var list<ReflectionAttribute> */
63: private $attributes;
64:
65: /** @var positive-int|null */
66: private $startLine;
67:
68: /** @var positive-int|null */
69: private $endLine;
70:
71: /** @var positive-int|null */
72: private $startColumn;
73:
74: /** @var positive-int|null */
75: private $endColumn;
76:
77: /** @psalm-allow-private-mutation
78: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
79: private $compiledDefaultValue = null;
80: /**
81: * @var \PHPStan\BetterReflection\Reflector\Reflector
82: */
83: private $reflector;
84: /**
85: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
86: */
87: private $declaringClass;
88: /**
89: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
90: */
91: private $implementingClass;
92: /**
93: * @var bool
94: */
95: private $isPromoted;
96: /**
97: * @var bool
98: */
99: private $declaredAtCompileTime;
100: private function __construct(Reflector $reflector, PropertyNode $node, Node\PropertyItem $propertyNode, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted, bool $declaredAtCompileTime)
101: {
102: $this->reflector = $reflector;
103: $this->declaringClass = $declaringClass;
104: $this->implementingClass = $implementingClass;
105: $this->isPromoted = $isPromoted;
106: $this->declaredAtCompileTime = $declaredAtCompileTime;
107: $this->name = $propertyNode->name->name;
108: $this->modifiers = $this->computeModifiers($node);
109: $this->type = $this->createType($node);
110: $this->default = $propertyNode->default;
111: $this->docComment = GetLastDocComment::forNode($node);
112: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
113: $startLine = $node->getStartLine();
114: if ($startLine === -1) {
115: $startLine = null;
116: }
117: $endLine = $node->getEndLine();
118: if ($endLine === -1) {
119: $endLine = null;
120: }
121: /** @psalm-suppress InvalidPropertyAssignmentValue */
122: $this->startLine = $startLine;
123: /** @psalm-suppress InvalidPropertyAssignmentValue */
124: $this->endLine = $endLine;
125: try {
126: $this->startColumn = CalculateReflectionColumn::getStartColumn($declaringClass->getLocatedSource()->getSource(), $node);
127: } catch (NoNodePosition $exception) {
128: $this->startColumn = null;
129: }
130: try {
131: $this->endColumn = CalculateReflectionColumn::getEndColumn($declaringClass->getLocatedSource()->getSource(), $node);
132: } catch (NoNodePosition $exception) {
133: $this->endColumn = null;
134: }
135: }
136:
137: /**
138: * Create a reflection of an instance's property by its name
139: *
140: * @param non-empty-string $propertyName
141: *
142: * @throws ReflectionException
143: * @throws IdentifierNotFound
144: * @throws OutOfBoundsException
145: */
146: public static function createFromInstance(object $instance, string $propertyName): self
147: {
148: $property = ReflectionClass::createFromInstance($instance)->getProperty($propertyName);
149:
150: if ($property === null) {
151: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
152: }
153:
154: return $property;
155: }
156:
157: /** @internal */
158: public function withImplementingClass(ReflectionClass $implementingClass): self
159: {
160: $clone = clone $this;
161: $clone->implementingClass = $implementingClass;
162:
163: if ($clone->type !== null) {
164: $clone->type = $clone->type->withOwner($clone);
165: }
166:
167: $clone->attributes = array_map(static function (ReflectionAttribute $attribute) use ($clone) : ReflectionAttribute {
168: return $attribute->withOwner($clone);
169: }, $this->attributes);
170:
171: $this->compiledDefaultValue = null;
172:
173: return $clone;
174: }
175:
176: /** @return non-empty-string */
177: public function __toString(): string
178: {
179: return ReflectionPropertyStringCast::toString($this);
180: }
181:
182: /**
183: * @internal
184: *
185: * @param PropertyNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
186: */
187: public static function createFromNode(Reflector $reflector, PropertyNode $node, Node\PropertyItem $propertyProperty, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted = false, bool $declaredAtCompileTime = true): self
188: {
189: return new self($reflector, $node, $propertyProperty, $declaringClass, $implementingClass, $isPromoted, $declaredAtCompileTime);
190: }
191:
192: /**
193: * Has the property been declared at compile-time?
194: *
195: * Note that unless the property is static, this is hard coded to return
196: * true, because we are unable to reflect instances of classes, therefore
197: * we can be sure that all properties are always declared at compile-time.
198: */
199: public function isDefault(): bool
200: {
201: return $this->declaredAtCompileTime;
202: }
203:
204: /**
205: * Get the core-reflection-compatible modifier values.
206: *
207: * @return int-mask-of<ReflectionPropertyAdapter::IS_*>
208: */
209: public function getModifiers(): int
210: {
211: return $this->modifiers;
212: }
213:
214: /**
215: * Get the name of the property.
216: *
217: * @return non-empty-string
218: */
219: public function getName(): string
220: {
221: return $this->name;
222: }
223:
224: /**
225: * Is the property private?
226: */
227: public function isPrivate(): bool
228: {
229: return ($this->modifiers & CoreReflectionProperty::IS_PRIVATE) === CoreReflectionProperty::IS_PRIVATE;
230: }
231:
232: /**
233: * Is the property protected?
234: */
235: public function isProtected(): bool
236: {
237: return ($this->modifiers & CoreReflectionProperty::IS_PROTECTED) === CoreReflectionProperty::IS_PROTECTED;
238: }
239:
240: /**
241: * Is the property public?
242: */
243: public function isPublic(): bool
244: {
245: return ($this->modifiers & CoreReflectionProperty::IS_PUBLIC) === CoreReflectionProperty::IS_PUBLIC;
246: }
247:
248: /**
249: * Is the property static?
250: */
251: public function isStatic(): bool
252: {
253: return ($this->modifiers & CoreReflectionProperty::IS_STATIC) === CoreReflectionProperty::IS_STATIC;
254: }
255:
256: public function isPromoted(): bool
257: {
258: return $this->isPromoted;
259: }
260:
261: public function isInitialized($object = null): bool
262: {
263: if ($object === null && $this->isStatic()) {
264: return ! $this->hasType() || $this->hasDefaultValue();
265: }
266:
267: try {
268: $this->getValue($object);
269:
270: return true;
271:
272: // @phpstan-ignore-next-line
273: } catch (Error $e) {
274: if (strpos($e->getMessage(), 'must not be accessed before initialization') !== false) {
275: return false;
276: }
277:
278: throw $e;
279: }
280: }
281:
282: public function isReadOnly(): bool
283: {
284: return ($this->modifiers & ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY) === ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY
285: || $this->getDeclaringClass()->isReadOnly();
286: }
287:
288: public function getDeclaringClass(): ReflectionClass
289: {
290: return $this->declaringClass;
291: }
292:
293: public function getImplementingClass(): ReflectionClass
294: {
295: return $this->implementingClass;
296: }
297:
298: /** @return non-empty-string|null */
299: public function getDocComment(): ?string
300: {
301: return $this->docComment;
302: }
303:
304: public function hasDefaultValue(): bool
305: {
306: return ! $this->hasType() || $this->default !== null;
307: }
308:
309: public function getDefaultValueExpression(): ?\PhpParser\Node\Expr
310: {
311: return $this->default;
312: }
313:
314: /**
315: * Get the default value of the property (as defined before constructor is
316: * called, when the property is defined)
317: * @return mixed
318: */
319: public function getDefaultValue()
320: {
321: if ($this->default === null) {
322: return null;
323: }
324:
325: if ($this->compiledDefaultValue === null) {
326: $this->compiledDefaultValue = (new CompileNodeToValue())->__invoke($this->default, new CompilerContext($this->reflector, $this));
327: }
328:
329: /** @psalm-var scalar|array<scalar>|null $value */
330: $value = $this->compiledDefaultValue->value;
331:
332: return $value;
333: }
334:
335: public function isDeprecated(): bool
336: {
337: return AnnotationHelper::isDeprecated($this->getDocComment());
338: }
339:
340: /**
341: * Get the line number that this property starts on.
342: *
343: * @return positive-int
344: *
345: * @throws CodeLocationMissing
346: */
347: public function getStartLine(): int
348: {
349: if ($this->startLine === null) {
350: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->implementingClass->getName()));
351: }
352:
353: return $this->startLine;
354: }
355:
356: /**
357: * Get the line number that this property ends on.
358: *
359: * @return positive-int
360: *
361: * @throws CodeLocationMissing
362: */
363: public function getEndLine(): int
364: {
365: if ($this->endLine === null) {
366: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->implementingClass->getName()));
367: }
368:
369: return $this->endLine;
370: }
371:
372: /**
373: * @return positive-int
374: *
375: * @throws CodeLocationMissing
376: */
377: public function getStartColumn(): int
378: {
379: if ($this->startColumn === null) {
380: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->implementingClass->getName()));
381: }
382:
383: return $this->startColumn;
384: }
385:
386: /**
387: * @return positive-int
388: *
389: * @throws CodeLocationMissing
390: */
391: public function getEndColumn(): int
392: {
393: if ($this->endColumn === null) {
394: throw CodeLocationMissing::create(sprintf('Was looking for property "$%s" in "%s".', $this->name, $this->implementingClass->getName()));
395: }
396:
397: return $this->endColumn;
398: }
399:
400: /** @return list<ReflectionAttribute> */
401: public function getAttributes(): array
402: {
403: return $this->attributes;
404: }
405:
406: /** @return list<ReflectionAttribute> */
407: public function getAttributesByName(string $name): array
408: {
409: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
410: }
411:
412: /**
413: * @param class-string $className
414: *
415: * @return list<ReflectionAttribute>
416: */
417: public function getAttributesByInstance(string $className): array
418: {
419: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
420: }
421:
422: /**
423: * @throws ClassDoesNotExist
424: * @throws NoObjectProvided
425: * @throws ObjectNotInstanceOfClass
426: * @return mixed
427: */
428: public function getValue($object = null)
429: {
430: $implementingClassName = $this->getImplementingClass()->getName();
431:
432: if ($this->isStatic()) {
433: $this->assertClassExist($implementingClassName);
434:
435: $closure = Closure::bind(function (string $implementingClassName, string $propertyName) {
436: return $implementingClassName::${$propertyName};
437: }, null, $implementingClassName);
438:
439: assert($closure instanceof Closure);
440:
441: return $closure->__invoke($implementingClassName, $this->getName());
442: }
443:
444: $instance = $this->assertObject($object);
445:
446: $closure = Closure::bind(function (object $instance, string $propertyName) {
447: return $instance->{$propertyName};
448: }, $instance, $implementingClassName);
449:
450: assert($closure instanceof Closure);
451:
452: return $closure->__invoke($instance, $this->getName());
453: }
454:
455: /**
456: * @throws ClassDoesNotExist
457: * @throws NoObjectProvided
458: * @throws NotAnObject
459: * @throws ObjectNotInstanceOfClass
460: * @param mixed $object
461: * @param mixed $value
462: */
463: public function setValue($object, $value = null): void
464: {
465: $implementingClassName = $this->getImplementingClass()->getName();
466:
467: if ($this->isStatic()) {
468: $this->assertClassExist($implementingClassName);
469:
470: $closure = Closure::bind(function (string $_implementingClassName, string $_propertyName, $value): void {
471: /** @psalm-suppress MixedAssignment */
472: $_implementingClassName::${$_propertyName} = $value;
473: }, null, $implementingClassName);
474:
475: assert($closure instanceof Closure);
476:
477: $closure->__invoke($implementingClassName, $this->getName(), func_num_args() === 2 ? $value : $object);
478:
479: return;
480: }
481:
482: $instance = $this->assertObject($object);
483:
484: $closure = Closure::bind(function (object $instance, string $propertyName, $value): void {
485: $instance->{$propertyName} = $value;
486: }, $instance, $implementingClassName);
487:
488: assert($closure instanceof Closure);
489:
490: $closure->__invoke($instance, $this->getName(), $value);
491: }
492:
493: /**
494: * Does this property allow null?
495: */
496: public function allowsNull(): bool
497: {
498: return $this->type === null || $this->type->allowsNull();
499: }
500:
501: /**
502: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
503: */
504: private function createType(PropertyNode $node)
505: {
506: $type = $node->type;
507:
508: if ($type === null) {
509: return null;
510: }
511:
512: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
513:
514: return ReflectionType::createFromNode($this->reflector, $this, $type);
515: }
516:
517: /**
518: * Get the ReflectionType instance representing the type declaration for
519: * this property
520: *
521: * (note: this has nothing to do with DocBlocks).
522: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
523: */
524: public function getType()
525: {
526: return $this->type;
527: }
528:
529: /**
530: * Does this property have a type declaration?
531: *
532: * (note: this has nothing to do with DocBlocks).
533: */
534: public function hasType(): bool
535: {
536: return $this->type !== null;
537: }
538:
539: /**
540: * @param class-string $className
541: *
542: * @throws ClassDoesNotExist
543: */
544: private function assertClassExist(string $className): void
545: {
546: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
547: throw new ClassDoesNotExist('Property cannot be retrieved as the class does not exist');
548: }
549: }
550:
551: /**
552: * @throws NoObjectProvided
553: * @throws NotAnObject
554: * @throws ObjectNotInstanceOfClass
555: *
556: * @psalm-assert object $object
557: * @param mixed $object
558: */
559: private function assertObject($object): object
560: {
561: if ($object === null) {
562: throw NoObjectProvided::create();
563: }
564:
565: if (! is_object($object)) {
566: throw NotAnObject::fromNonObject($object);
567: }
568:
569: $implementingClassName = $this->getImplementingClass()->getName();
570:
571: if (get_class($object) !== $implementingClassName) {
572: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
573: }
574:
575: return $object;
576: }
577:
578: /** @return int-mask-of<ReflectionPropertyAdapter::IS_*> */
579: private function computeModifiers(PropertyNode $node): int
580: {
581: $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY : 0;
582: $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0;
583: $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0;
584: $modifiers += $node->isProtected() ? CoreReflectionProperty::IS_PROTECTED : 0;
585: $modifiers += $node->isPublic() ? CoreReflectionProperty::IS_PUBLIC : 0;
586:
587: return $modifiers;
588: }
589: }
590: