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: class ReflectionProperty
41: {
42: /** @var non-empty-string */
43: private $name;
44:
45: /** @var int-mask-of<ReflectionPropertyAdapter::IS_*> */
46: private $modifiers;
47:
48: /**
49: * @var \PHPStan\BetterReflection\Reflection\ReflectionNamedType|\PHPStan\BetterReflection\Reflection\ReflectionUnionType|\PHPStan\BetterReflection\Reflection\ReflectionIntersectionType|null
50: */
51: private $type;
52:
53: /**
54: * @var \PhpParser\Node\Expr|null
55: */
56: private $default;
57:
58: /**
59: * @var string|null
60: */
61: private $docComment;
62:
63: /** @var list<ReflectionAttribute> */
64: private $attributes;
65:
66: /** @var positive-int|null */
67: private $startLine;
68:
69: /** @var positive-int|null */
70: private $endLine;
71:
72: /** @var positive-int|null */
73: private $startColumn;
74:
75: /** @var positive-int|null */
76: private $endColumn;
77:
78: /**
79: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null
80: */
81: private $compiledDefaultValue = null;
82: /**
83: * @var \PHPStan\BetterReflection\Reflector\Reflector
84: */
85: private $reflector;
86: /**
87: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
88: */
89: private $declaringClass;
90: /**
91: * @var \PHPStan\BetterReflection\Reflection\ReflectionClass
92: */
93: private $implementingClass;
94: /**
95: * @var bool
96: */
97: private $isPromoted;
98: /**
99: * @var bool
100: */
101: private $declaredAtCompileTime;
102: private function __construct(Reflector $reflector, PropertyNode $node, Node\Stmt\PropertyProperty $propertyNode, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted, bool $declaredAtCompileTime)
103: {
104: $this->reflector = $reflector;
105: $this->declaringClass = $declaringClass;
106: $this->implementingClass = $implementingClass;
107: $this->isPromoted = $isPromoted;
108: $this->declaredAtCompileTime = $declaredAtCompileTime;
109: $name = $propertyNode->name->name;
110: assert($name !== '');
111: $this->name = $name;
112: $this->modifiers = $this->computeModifiers($node);
113: $this->type = $this->createType($node);
114: $this->default = $propertyNode->default;
115: $this->docComment = GetLastDocComment::forNode($node);
116: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
117: $startLine = null;
118: if ($node->hasAttribute('startLine')) {
119: $startLine = $node->getStartLine();
120: assert($startLine > 0);
121: }
122: $endLine = null;
123: if ($node->hasAttribute('endLine')) {
124: $endLine = $node->getEndLine();
125: assert($endLine > 0);
126: }
127: $this->startLine = $startLine;
128: $this->endLine = $endLine;
129: try {
130: $this->startColumn = CalculateReflectionColumn::getStartColumn($declaringClass->getLocatedSource()->getSource(), $node);
131: } catch (NoNodePosition $exception) {
132: $this->startColumn = null;
133: }
134: try {
135: $this->endColumn = CalculateReflectionColumn::getEndColumn($declaringClass->getLocatedSource()->getSource(), $node);
136: } catch (NoNodePosition $exception) {
137: $this->endColumn = null;
138: }
139: }
140:
141: /**
142: * Create a reflection of a class's property by its name
143: *
144: * @param non-empty-string $propertyName
145: *
146: * @throws OutOfBoundsException
147: */
148: public static function createFromName(string $className, string $propertyName): self
149: {
150: $property = ReflectionClass::createFromName($className)->getProperty($propertyName);
151:
152: if ($property === null) {
153: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
154: }
155:
156: return $property;
157: }
158:
159: /**
160: * Create a reflection of an instance's property by its name
161: *
162: * @param non-empty-string $propertyName
163: *
164: * @throws ReflectionException
165: * @throws IdentifierNotFound
166: * @throws OutOfBoundsException
167: */
168: public static function createFromInstance(object $instance, string $propertyName): self
169: {
170: $property = ReflectionClass::createFromInstance($instance)->getProperty($propertyName);
171:
172: if ($property === null) {
173: throw new OutOfBoundsException(sprintf('Could not find property: %s', $propertyName));
174: }
175:
176: return $property;
177: }
178:
179: /** @internal */
180: public function withImplementingClass(ReflectionClass $implementingClass): self
181: {
182: $clone = clone $this;
183: $clone->implementingClass = $implementingClass;
184:
185: if ($clone->type !== null) {
186: $clone->type = $clone->type->withOwner($clone);
187: }
188:
189: $clone->attributes = array_map(static function (ReflectionAttribute $attribute) use ($clone) : ReflectionAttribute {
190: return $attribute->withOwner($clone);
191: }, $this->attributes);
192:
193: $this->compiledDefaultValue = null;
194:
195: return $clone;
196: }
197:
198: public function __toString(): string
199: {
200: return ReflectionPropertyStringCast::toString($this);
201: }
202:
203: /**
204: * @internal
205: *
206: * @param PropertyNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
207: */
208: public static function createFromNode(Reflector $reflector, PropertyNode $node, Node\Stmt\PropertyProperty $propertyProperty, ReflectionClass $declaringClass, ReflectionClass $implementingClass, bool $isPromoted = false, bool $declaredAtCompileTime = true): self
209: {
210: return new self($reflector, $node, $propertyProperty, $declaringClass, $implementingClass, $isPromoted, $declaredAtCompileTime);
211: }
212:
213: /**
214: * Has the property been declared at compile-time?
215: *
216: * Note that unless the property is static, this is hard coded to return
217: * true, because we are unable to reflect instances of classes, therefore
218: * we can be sure that all properties are always declared at compile-time.
219: */
220: public function isDefault(): bool
221: {
222: return $this->declaredAtCompileTime;
223: }
224:
225: /**
226: * Get the core-reflection-compatible modifier values.
227: *
228: * @return int-mask-of<ReflectionPropertyAdapter::IS_*>
229: */
230: public function getModifiers(): int
231: {
232: return $this->modifiers;
233: }
234:
235: /**
236: * Get the name of the property.
237: *
238: * @return non-empty-string
239: */
240: public function getName(): string
241: {
242: return $this->name;
243: }
244:
245: /**
246: * Is the property private?
247: */
248: public function isPrivate(): bool
249: {
250: return ($this->modifiers & CoreReflectionProperty::IS_PRIVATE) === CoreReflectionProperty::IS_PRIVATE;
251: }
252:
253: /**
254: * Is the property protected?
255: */
256: public function isProtected(): bool
257: {
258: return ($this->modifiers & CoreReflectionProperty::IS_PROTECTED) === CoreReflectionProperty::IS_PROTECTED;
259: }
260:
261: /**
262: * Is the property public?
263: */
264: public function isPublic(): bool
265: {
266: return ($this->modifiers & CoreReflectionProperty::IS_PUBLIC) === CoreReflectionProperty::IS_PUBLIC;
267: }
268:
269: /**
270: * Is the property static?
271: */
272: public function isStatic(): bool
273: {
274: return ($this->modifiers & CoreReflectionProperty::IS_STATIC) === CoreReflectionProperty::IS_STATIC;
275: }
276:
277: public function isPromoted(): bool
278: {
279: return $this->isPromoted;
280: }
281:
282: public function isInitialized($object = null): bool
283: {
284: if ($object === null && $this->isStatic()) {
285: return ! $this->hasType() || $this->hasDefaultValue();
286: }
287:
288: try {
289: $this->getValue($object);
290:
291: return true;
292:
293: // @phpstan-ignore-next-line
294: } catch (Error $e) {
295: if (strpos($e->getMessage(), 'must not be accessed before initialization') !== false) {
296: return false;
297: }
298:
299: throw $e;
300: }
301: }
302:
303: public function isReadOnly(): bool
304: {
305: return ($this->modifiers & ReflectionPropertyAdapter::IS_READONLY) === ReflectionPropertyAdapter::IS_READONLY
306: || $this->getDeclaringClass()->isReadOnly();
307: }
308:
309: public function getDeclaringClass(): ReflectionClass
310: {
311: return $this->declaringClass;
312: }
313:
314: public function getImplementingClass(): ReflectionClass
315: {
316: return $this->implementingClass;
317: }
318:
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: