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 catch.neverThrown */
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: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
440: assert($closure instanceof Closure);
441:
442: return $closure->__invoke($implementingClassName, $this->getName());
443: }
444:
445: $instance = $this->assertObject($object);
446:
447: $closure = Closure::bind(function (object $instance, string $propertyName) {
448: return $instance->{$propertyName};
449: }, $instance, $implementingClassName);
450:
451: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
452: assert($closure instanceof Closure);
453:
454: return $closure->__invoke($instance, $this->getName());
455: }
456:
457: /**
458: * @throws ClassDoesNotExist
459: * @throws NoObjectProvided
460: * @throws NotAnObject
461: * @throws ObjectNotInstanceOfClass
462: * @param mixed $object
463: * @param mixed $value
464: */
465: public function setValue($object, $value = null): void
466: {
467: $implementingClassName = $this->getImplementingClass()->getName();
468:
469: if ($this->isStatic()) {
470: $this->assertClassExist($implementingClassName);
471:
472: $closure = Closure::bind(function (string $_implementingClassName, string $_propertyName, $value): void {
473: /** @psalm-suppress MixedAssignment */
474: $_implementingClassName::${$_propertyName} = $value;
475: }, null, $implementingClassName);
476:
477: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
478: assert($closure instanceof Closure);
479:
480: $closure->__invoke($implementingClassName, $this->getName(), func_num_args() === 2 ? $value : $object);
481:
482: return;
483: }
484:
485: $instance = $this->assertObject($object);
486:
487: $closure = Closure::bind(function (object $instance, string $propertyName, $value): void {
488: $instance->{$propertyName} = $value;
489: }, $instance, $implementingClassName);
490:
491: /** @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue */
492: assert($closure instanceof Closure);
493:
494: $closure->__invoke($instance, $this->getName(), $value);
495: }
496:
497: /**
498: * Does this property allow null?
499: */
500: public function allowsNull(): bool
501: {
502: return $this->type === null || $this->type->allowsNull();
503: }
504:
505: /**
506: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
507: */
508: private function createType(PropertyNode $node)
509: {
510: $type = $node->type;
511:
512: if ($type === null) {
513: return null;
514: }
515:
516: assert($type instanceof Node\Identifier || $type instanceof Node\Name || $type instanceof Node\NullableType || $type instanceof Node\UnionType || $type instanceof Node\IntersectionType);
517:
518: return ReflectionType::createFromNode($this->reflector, $this, $type);
519: }
520:
521: /**
522: * Get the ReflectionType instance representing the type declaration for
523: * this property
524: *
525: * (note: this has nothing to do with DocBlocks).
526: * @return \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
527: */
528: public function getType()
529: {
530: return $this->type;
531: }
532:
533: /**
534: * Does this property have a type declaration?
535: *
536: * (note: this has nothing to do with DocBlocks).
537: */
538: public function hasType(): bool
539: {
540: return $this->type !== null;
541: }
542:
543: /**
544: * @param class-string $className
545: *
546: * @throws ClassDoesNotExist
547: */
548: private function assertClassExist(string $className): void
549: {
550: if (! ClassExistenceChecker::classExists($className, true) && ! ClassExistenceChecker::traitExists($className, true)) {
551: throw new ClassDoesNotExist('Property cannot be retrieved as the class does not exist');
552: }
553: }
554:
555: /**
556: * @throws NoObjectProvided
557: * @throws NotAnObject
558: * @throws ObjectNotInstanceOfClass
559: *
560: * @psalm-assert object $object
561: * @param mixed $object
562: */
563: private function assertObject($object): object
564: {
565: if ($object === null) {
566: throw NoObjectProvided::create();
567: }
568:
569: if (! is_object($object)) {
570: throw NotAnObject::fromNonObject($object);
571: }
572:
573: $implementingClassName = $this->getImplementingClass()->getName();
574:
575: if (get_class($object) !== $implementingClassName) {
576: throw ObjectNotInstanceOfClass::fromClassName($implementingClassName);
577: }
578:
579: return $object;
580: }
581:
582: /** @return int-mask-of<ReflectionPropertyAdapter::IS_*> */
583: private function computeModifiers(PropertyNode $node): int
584: {
585: $modifiers = $node->isReadonly() ? ReflectionPropertyAdapter::IS_READONLY_COMPATIBILITY : 0;
586: $modifiers += $node->isStatic() ? CoreReflectionProperty::IS_STATIC : 0;
587: $modifiers += $node->isPrivate() ? CoreReflectionProperty::IS_PRIVATE : 0;
588: $modifiers += $node->isProtected() ? CoreReflectionProperty::IS_PROTECTED : 0;
589: $modifiers += $node->isPublic() ? CoreReflectionProperty::IS_PUBLIC : 0;
590:
591: return $modifiers;
592: }
593: }
594: