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