Skip to content

Commit 961edcc

Browse files
committed
fix: check magic methods on final classes for allow dynamic properties
1 parent e0c4844 commit 961edcc

File tree

3 files changed

+90
-34
lines changed

3 files changed

+90
-34
lines changed

src/Reflection/ClassReflection.php

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -421,22 +421,6 @@ public function allowsDynamicProperties(): bool
421421
return true;
422422
}
423423

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-
440424
$hasMagicMethod = $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset');
441425
if ($hasMagicMethod) {
442426
return true;
@@ -449,18 +433,20 @@ private function allowsDynamicPropertiesExtensions(): bool
449433
}
450434

451435
$reflection = $type->getClassReflection();
452-
if ($reflection === null) {
453-
continue;
454-
}
455-
456-
if (!$reflection->allowsDynamicPropertiesExtensions()) {
436+
if ($reflection === null || !$reflection->allowsDynamicProperties()) {
457437
continue;
458438
}
459439

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

tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -821,23 +821,23 @@ 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+
];
835840
}
836-
$errors[] = [
837-
'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.',
838-
112,
839-
$tipText,
840-
];
841841
} elseif ($b) {
842842
$errors[] = [
843843
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',

0 commit comments

Comments
 (0)