Stringable object is a valid name, a non-stringable object is a TypeError.
Method names must be actual strings — even __toString() is not enough
("Method name must be a string").
PHPStan 2.2.15 reports the non-stringable cases on bleeding edge
(checkNonStringableDynamicAccess, phpstan-src#5871 / phpstan#4710). Mago
1.50.0 started accepting Stringable property names (and variable-variable
reads). Variable-variables themselves are not probed here: phpstan-strict
bans $$ outright, which would fire on the Stringable control for a reason
that is not the name's type.
References:
- phpstan-src#5871
- phpstan#4710
- https://github.com/carthage-software/mago/releases/tag/1.50.0| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.10Phan 6.0.10 php-ast version 1.1.3 PHP version used to run Phan: 8.5.11 | Pass | regressions_dynamic_name_stringable.php:57: Saw a dynamic usage of an instance property with a name of type \Conformance\Tests\RegressionsDynamicNameStringable\Name but expected the name to be a string [PhanTypeInvalidPropertyName] regressions_dynamic_name_stringable.php:62: Saw a dynamic usage of an instance property with a name of type \Conformance\Tests\RegressionsDynamicNameStringable\NotStringable but expected the name to be a string [PhanTypeInvalidPropertyName] regressions_dynamic_name_stringable.php:72: Instance method name must be a string, got \Conformance\Tests\RegressionsDynamicNameStringable\Name [PhanTypeInvalidMethodName] regressions_dynamic_name_stringable.php:77: Instance method name must be a string, got \Conformance\Tests\RegressionsDynamicNameStringable\NotStringable [PhanTypeInvalidMethodName] |
| phpstan | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Pass reported Lv.2+ | regressions_dynamic_name_stringable.php:62: Property name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. [reported-from-level=2] regressions_dynamic_name_stringable.php:72: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\Name was given. [reported-from-level=2] regressions_dynamic_name_stringable.php:77: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. [reported-from-level=2] With strict-rulesregressions_dynamic_name_stringable.php:57: Variable property access on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:62: Property name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. regressions_dynamic_name_stringable.php:62: Variable property access on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:72: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\Name was given. regressions_dynamic_name_stringable.php:72: Variable method call on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:77: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. regressions_dynamic_name_stringable.php:77: Variable method call on Conformance\Tests\RegressionsDynamicNameStringable\Target. |
| psalm | 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c next: 7.0.0-rc2 |
Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| mago | 1.57.0mago 1.57.0 | Pass | regressions_dynamic_name_stringable.php:57: This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined. [string-member-selector] regressions_dynamic_name_stringable.php:62: Cannot cast object of type `Conformance\Tests\RegressionsDynamicNameStringable\NotStringable` to `string` because it does not implement `Stringable`. [invalid-type-cast] regressions_dynamic_name_stringable.php:62: This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined. [string-member-selector] regressions_dynamic_name_stringable.php:72: Invalid type `Conformance\Tests\RegressionsDynamicNameStringable\Name` used as a member selector; a string is expected. [invalid-member-selector] regressions_dynamic_name_stringable.php:77: Invalid type `Conformance\Tests\RegressionsDynamicNameStringable\NotStringable` used as a member selector; a string is expected. [invalid-member-selector] |
| mir | 0.87.0mir 0.87.0 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| phpantom | 0.11.1phpantom_lsp 0.11.1 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| intelephense | 1.18.5intelephense 1.18.5 | Fail | regressions_dynamic_name_stringable.php:62: Expected type 'string|Stringable'. Found 'Conformance\Tests\RegressionsDynamicNameStringable\NotStringable'. [P1006] Expectation diffLine 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| phpactor | 2026.10.07.0Phpactor 2026.10.07.0 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| phpy | 1.0.19409phpy 1.0.19409 | Fail | regressions_dynamic_name_stringable.php:72: Illegal function name of type 'Conformance\Tests\RegressionsDynamicNameStringable\Name', name must be of type string regressions_dynamic_name_stringable.php:77: Illegal function name of type 'Conformance\Tests\RegressionsDynamicNameStringable\NotStringable', name must be of type string Expectation diffLine 62: Expected 1 error(s) |
| qodana | 262.10968.76PhpStorm 262.10968.76 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
| phpstan-strict | 2.3.1PHPStan - PHP Static Analysis Tool 2.3.1 | Pass | regressions_dynamic_name_stringable.php:57: Variable property access on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:62: Property name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. regressions_dynamic_name_stringable.php:62: Variable property access on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:72: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\Name was given. regressions_dynamic_name_stringable.php:72: Variable method call on Conformance\Tests\RegressionsDynamicNameStringable\Target. regressions_dynamic_name_stringable.php:77: Method name for Conformance\Tests\RegressionsDynamicNameStringable\Target must be a string, but Conformance\Tests\RegressionsDynamicNameStringable\NotStringable was given. regressions_dynamic_name_stringable.php:77: Variable method call on Conformance\Tests\RegressionsDynamicNameStringable\Target. |
| psalm-next | 7.0.0-rc2Psalm 7.0.0-rc2@20339e3fe5b8d03bcd8061030eb2a83ad50e5783 | Fail | No diagnostics reported. Expectation diffLine 62: Expected 1 error(s) Line 72: Expected 1 error(s) Line 77: Expected 1 error(s) |
<?php
declare(strict_types=1);
namespace Conformance\Tests\RegressionsDynamicNameStringable;
/**
* Dynamic names are not all string-cast the same way.
*
* Property names (and variable-variables) go through string casting: a
* `Stringable` object is a valid name, a non-stringable object is a TypeError.
* Method names must be actual strings — even `__toString()` is not enough
* ("Method name must be a string").
*
* PHPStan 2.2.15 reports the non-stringable cases on bleeding edge
* (`checkNonStringableDynamicAccess`, phpstan-src#5871 / phpstan#4710). Mago
* 1.50.0 started accepting `Stringable` property names (and variable-variable
* reads). Variable-variables themselves are not probed here: phpstan-strict
* bans `$$` outright, which would fire on the Stringable control for a reason
* that is not the name's type.
*
* References:
* - https://github.com/phpstan/phpstan-src/pull/5871
* - https://github.com/phpstan/phpstan/issues/4710
* - https://github.com/carthage-software/mago/releases/tag/1.50.0
*/
final class Name implements \Stringable
{
#[\Override]
public function __toString(): string
{
return 'id';
}
}
final class NotStringable
{
}
final class Target
{
public int $id = 1;
public function id(): int
{
return 1;
}
}
function takesMixed(mixed $value): void
{
}
function propertyStringable(Name $name, Target $target): void
{
takesMixed($target->{$name}); // Q?: PHP string-casts Stringable property names. mago then warns that the resulting string is not a literal; that warning is not a rejection of Stringable
}
function propertyNonStringable(NotStringable $name, Target $target): void
{
takesMixed($target->{$name}); // E: non-stringable object is not a property name
}
function methodString(Target $target): void
{
takesMixed($target->id()); // V: the method exists when named statically. Dynamic string names are the E lines; phpstan-strict bans `$obj->{$name}()` itself
}
function methodStringable(Name $name, Target $target): void
{
takesMixed($target->{$name}()); // E: method names must be strings, not Stringable
}
function methodNonStringable(NotStringable $name, Target $target): void
{
takesMixed($target->{$name}()); // E: non-stringable object is not a method name
}