Skip to content

Commit a15515c

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

File tree

3 files changed

+92
-34
lines changed

3 files changed

+92
-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: 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: 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)