Skip to content

Commit 207639f

Browse files
calebdwondrejmirtes
authored andcommitted
fix: check magic methods on final classes for allow dynamic properties
1 parent 785ed70 commit 207639f

File tree

4 files changed

+128
-44
lines changed

4 files changed

+128
-44
lines changed

src/Reflection/ClassReflection.php

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -410,33 +410,6 @@ public function allowsDynamicProperties(): bool
410410
return true;
411411
}
412412

413-
if ($this->isReadOnly()) {
414-
return false;
415-
}
416-
417-
if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
418-
$this->reflectionProvider,
419-
$this,
420-
)) {
421-
return true;
422-
}
423-
424-
$class = $this;
425-
$attributes = $class->reflection->getAttributes('AllowDynamicProperties');
426-
while (count($attributes) === 0 && $class->getParentClass() !== null) {
427-
$attributes = $class->getParentClass()->reflection->getAttributes('AllowDynamicProperties');
428-
$class = $class->getParentClass();
429-
}
430-
431-
return count($attributes) > 0;
432-
}
433-
434-
private function allowsDynamicPropertiesExtensions(): bool
435-
{
436-
if ($this->allowsDynamicProperties()) {
437-
return true;
438-
}
439-
440413
$hasMagicMethod = $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset');
441414
if ($hasMagicMethod) {
442415
return true;
@@ -449,18 +422,31 @@ private function allowsDynamicPropertiesExtensions(): bool
449422
}
450423

451424
$reflection = $type->getClassReflection();
452-
if ($reflection === null) {
425+
if ($reflection === null || !$reflection->allowsDynamicProperties()) {
453426
continue;
454427
}
455428

456-
if (!$reflection->allowsDynamicPropertiesExtensions()) {
457-
continue;
458-
}
429+
return true;
430+
}
431+
432+
if ($this->isReadOnly()) {
433+
return false;
434+
}
459435

436+
if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
437+
$this->reflectionProvider,
438+
$this,
439+
)) {
460440
return true;
461441
}
462442

463-
return false;
443+
$class = $this;
444+
do {
445+
$attributes = $class->reflection->getAttributes('AllowDynamicProperties');
446+
$class = $class->getParentClass();
447+
} while ($attributes === [] && $class !== null);
448+
449+
return $attributes !== [];
464450
}
465451

466452
public function hasProperty(string $propertyName): bool
@@ -474,7 +460,7 @@ public function hasProperty(string $propertyName): bool
474460
}
475461

476462
foreach ($this->propertiesClassReflectionExtensions as $i => $extension) {
477-
if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) {
463+
if ($i > 0 && !$this->allowsDynamicProperties()) {
478464
break;
479465
}
480466
if ($extension->hasProperty($this, $propertyName)) {
@@ -656,7 +642,7 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco
656642

657643
if (!isset($this->properties[$key])) {
658644
foreach ($this->propertiesClassReflectionExtensions as $i => $extension) {
659-
if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) {
645+
if ($i > 0 && !$this->allowsDynamicProperties()) {
660646
break;
661647
}
662648

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13450;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TRelated of Model
9+
* @template TDeclaring of Model
10+
* @template TResult
11+
*/
12+
abstract class Relation
13+
{
14+
/** @return TResult */
15+
public function getResults(): mixed
16+
{
17+
return []; // @phpstan-ignore return.type
18+
}
19+
}
20+
21+
/**
22+
* @template TRelated of Model
23+
* @template TDeclaring of Model
24+
* @template TPivot of Pivot = Pivot
25+
* @template TAccessor of string = 'pivot'
26+
*
27+
* @extends Relation<TRelated, TDeclaring, array<int, TRelated&object{pivot: TPivot}>>
28+
*/
29+
class BelongsToMany extends Relation {}
30+
31+
abstract class Model
32+
{
33+
/**
34+
* @template TRelated of Model
35+
* @param class-string<TRelated> $related
36+
* @return BelongsToMany<TRelated, $this>
37+
*/
38+
public function belongsToMany(string $related): BelongsToMany
39+
{
40+
return new BelongsToMany(); // @phpstan-ignore return.type
41+
}
42+
43+
public function __get(string $name): mixed { return null; }
44+
public function __set(string $name, mixed $value): void {}
45+
}
46+
47+
class Pivot extends Model {}
48+
49+
class User extends Model
50+
{
51+
/** @return BelongsToMany<Team, $this> */
52+
public function teams(): BelongsToMany
53+
{
54+
return $this->belongsToMany(Team::class);
55+
}
56+
57+
/** @return BelongsToMany<TeamFinal, $this> */
58+
public function teamsFinal(): BelongsToMany
59+
{
60+
return $this->belongsToMany(TeamFinal::class);
61+
}
62+
}
63+
64+
class Team extends Model {}
65+
66+
final class TeamFinal extends Model {}
67+
68+
function test(User $user): void
69+
{
70+
assertType('array<int, Bug13450\Team&object{pivot: Bug13450\Pivot}>', $user->teams()->getResults());
71+
assertType('array<int, Bug13450\TeamFinal&object{pivot: Bug13450\Pivot}>', $user->teamsFinal()->getResults());
72+
}

tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -821,23 +821,28 @@ public function testPhp82AndDynamicProperties(bool $b): void
821821
34,
822822
$tipText,
823823
];
824-
$errors[] = [
825-
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
826-
71,
827-
$tipText,
828-
];
829824
if ($b) {
825+
$errors[] = [
826+
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
827+
71,
828+
$tipText,
829+
];
830830
$errors[] = [
831831
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
832832
78,
833833
$tipText,
834834
];
835+
$errors[] = [
836+
'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.',
837+
112,
838+
$tipText,
839+
];
840+
$errors[] = [
841+
'Access to an undefined property Php82DynamicProperties\ReadonlyWithMagic::$foo.',
842+
133,
843+
$tipText,
844+
];
835845
}
836-
$errors[] = [
837-
'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.',
838-
112,
839-
$tipText,
840-
];
841846
} elseif ($b) {
842847
$errors[] = [
843848
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',

tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,24 @@ function (): void {
114114
echo $hello->world;
115115
}
116116
};
117+
118+
readonly class ReadonlyWithMagic
119+
{
120+
public function __set(string $name, mixed $value): void
121+
{
122+
var_dump('here');
123+
}
124+
125+
public function __get(string $name): mixed
126+
{
127+
return 1;
128+
}
129+
}
130+
131+
function (): void {
132+
$class = new ReadonlyWithMagic();
133+
if(isset($class->foo))
134+
{
135+
echo $class->foo;
136+
}
137+
};

0 commit comments

Comments
 (0)