← All results

Dynamic names are not all string-cast the same way

Group: Cross-analyzer regressions · Category: regression · File: regressions_dynamic_name_stringable.php

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: - phpstan-src#5871 - phpstan#4710 - https://github.com/carthage-software/mago/releases/tag/1.50.0

Analyzer results

AnalyzerVersionResultDiagnostics
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-rules
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 6.20.0Psalm 6.20.0@804f03c0c183d149fcd4b8c7c0c9b3fbf836d34c
next: 7.0.0-rc2
Fail

No diagnostics reported.

Expectation diff
Line 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 diff
Line 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 diff
Line 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 diff
Line 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 diff
Line 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 diff
Line 62: Expected 1 error(s)
qodana 262.10968.76PhpStorm 262.10968.76 Fail

No diagnostics reported.

Expectation diff
Line 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 diff
Line 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 diff
Line 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 diff
Line 62: Expected 1 error(s)
Line 72: Expected 1 error(s)
Line 77: Expected 1 error(s)

Source

<?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
}