Skip to content

Equivalent to "::class" for obtaining property and variable names as a string #9449

@boenrobot

Description

@boenrobot

Description

I understand this will require an RFC, but I don't have any know how on how to implement this (I'm just a poor PHP user), so I thought I outline the issue, and hopefully inspire someone more familiar with the engine to pick it up. Maybe not necessarily end with the proposed design.

Problem

There is no way to get a reference to a property or variable symbol in a way that is subject to static analysis and/or easy refactors.

Precedent

Objects and classes

Objects and class symbols have ::class that gives the name of a class. IDEs can rename the class with this also including usages of ::class, ensuring that if a class name was expected, it would still point to the correct class after a rename. Static analyzers can warn if ::class is not used for functions/methods that are annotated as accepting a class string or the string is otherwise wrong (e.g. in PHPStan).

Methods and functions

Since PHP 8.1, methods and function symbols now have (...) that gives a callable. Again, this allows IDEs to rename a method/function while keeping the references to it valid, and also allows static analyzers to match it with annotated callables and report signature inconsistencies or a missing function in the case of plain strings (e.g. in PHPStan).

Properties and variables

There is no way in which you can reference the name of a property or a variable without resorting to an unannotated string, which breaks static analyzers. In the case of IDEs, they may try to be a bit smarter, but can also cause false renames in unrelated strings.

The closest thing one can do today would be to have the property/variable name in a constant, and then use the constant (plus for PHPStan to add an annotation for a property/variable name). Even this approach however would keep the value of the constant and the actual variable name not necessarily tied. One at bare minimum needs to change the property name and the constant value, while ensuring all places use the constant. It's also a bit less DX friendly - you are adding an extra boilerplate symbol - a constant - to address a limitation of the engine. This is just as unfriendly as doing the same for classes prior to the days of ::class.

Use cases

The biggest practical use use today is in ORMs like doctrine. They rely on properties to hold the column values. Those property names are in turn referenced in queries, so that the query builder can build the proper query. One can't simply rename a property in a doctrine entity without also going through any place in the code that this property may be used. Admittedly, in a well organized code base, that would probably be just one file - the repository class, but even then - it may be multiple times in it, with a simple search & replace potentially being error prone if the property name is shared with that of a related entity also used in the same repo.

Specialized doctrine IDE plugins may help with that, but it would be better overall for the PHP ecosystem if any library could do this, for any purpose, without a specialized plugin. And the aforementioned approach of storing property names as constants already adds a ton of boilerplate to the already very boilerplate-y doctrine entities. Most developers don't see enough value to justify it, until it is long late to introduce it.

Proposal

Enable the use of a keyword combined with :: or some more exotic syntax, that would take a variable or property and produce results similar to ::class - produce a string that is the property/variable name. Once this name is obtained, it could later be used in dynamic property accessors or variable variables.

The keyword var will be used to illustrate the point - it sounds like a relatively intuitive keyword for the use case, is an existing keyword, and it's short.

With regards to protected and private properties, and the interaction with __get and __set magic methods, for best DX, this syntax should allow the access of protected and private property names, even outside the object, but only if they are available inside the referenced object, and only if they are NOT dynamically defined or otherwise only accessible via magic methods. If one tries to actually access the protected/private property of that name (not just get its name), they are still subject to the normal access rules, same as if a literal string was supplied.

e.g.

$myVar = 0;

class MyClass {
    public static $myStaticProp = 1;
    protected static $myProtectedStaticProp = 2;
    private static $myPrivateStaticProp = 3;

    public $myProp = 4;
    protected $myProtectedProp = 5;
    private $myPrivateProp = 6;

    public function out(string $prop)
    {
        return $this->{$prop};
    }
}

class MyExtendedClass extends MyClass {
    public static $myExtendedStaticProp = 11;
    protected static $myExtendedProtectedStaticProp = 12;
    private static $myExtendedPrivateStaticProp = 13;

    public $myExtendedProp = 14;
    protected $myExtendedProtectedProp = 15;
    private $myExtendedPrivateProp = 16;

    public function outExt(string $prop)
    {
        return $this->{$prop};
    }
}

$varName = var::$myVar; // $varName = 'myVar'
echo ${$varName};//Outputs 0

$publicStaticPropName = var::MyClass::$myStaticProp; // $publicStaticPropName = 'myStaticProp'
echo MyClass::${$publicStaticPropName};//Outputs 1

$protectedStaticPropName = var::MyClass::$myProtectedStaticProp; // $protectedStaticPropName = 'myProtectedStaticProp'
echo MyClass::${$protectedStaticPropName};//Runtime error; Protected static property is not accessible here

$className = MyExtendedClass::class;
$obj = new $className();
$propName = var::$obj->myProp; // $propName = 'myProp'
echo $obj->{$propName};//Outputs 4

$protectedPropName = var::$obj->myProtectedProp; // $protectedPropName = 'myProtectedProp'
echo $obj->{$protectedPropName};// Runtime error; Protected property is not accessible here
echo $obj->out($protectedPropName); //Outputs 5

$privatePropName = var::$obj->myPrivateProp; // Runtime error; myPrivateProp is not available from MyExtendedClass
$extendedPrivatePropName = $obj->myExtendedPrivateProp::use; // $extendedPrivatePropName = 'myExtendedPrivateProp'
echo $obj->outExt($extendedPrivatePropName);//Outputs 16

Syntax choice

Due to the change in access rules for this (protected and private properties being allowed), a prefix is used. This enables IDEs to filter suggestions early, and for PHP itself to know that it will get a reference to a variable or property symbol on the right hand side of ::, which in turn removes potential ambiguities. Basically, the allowed forms on the right of var:: are variables accessible in the current scope, or any expression ending with "::$" or "->" on the same expression level. Anything else, including a single bracketed statement without anything after it can be assumed to evaluate to a literal, rather than a property or variable symbol.

e.g.

//sugar
$varName = var::(new $className)->myProp;
//desugar
new $className
$varName = 'myProp'

//sugar
$obj->{(var::(new $className)->myPropPointingToObject)}->myProp;
//desugar
new $className;
$obj->myPropPointingToObject->myProp;

//sugar
$obj->{(var::MyRelatedClass::$myRelatedProp) . 'Key'}->myProp
//desugar
$obj->myRelatedPropKey->myProp

// sugar
$obj->{var::$myVar};
//desugar
$obj->myVar;

// sagar
$obj->{var::($myVar)->myProp};
//desugar
$obj->myProp;

//sugar
$obj->{var::$myVar}::class
//desugar
$obj->myVar::class

//sugar
$obj->{var::$myVar}(...)
//desugar
$obj->myVar(...)


//sugar
$obj->{var::MyRelatedClass::${(var::MyOtherRelatedClassClass::myOtherRelatedProp) . 'Key'}}->myProp
//desugar inner most
$obj->{var::MyRelatedClass::${'myOtherRelatedPropKey'}}->myProp
//desugar further
$obj->myOtherRelatedPropKey->myProp

// A less useful in real situation case, but still perfectly fine syntax wise
//sugar
$obj->{(var::MyRelatedClass::${var::MyOtherRelatedClassClass::myOtherRelatedProp}) . 'Key'}->myProp
//desugar inner most
$obj->{(var::MyRelatedClass::${'myOtherRelatedProp'}) . 'Key'}->myProp
//desugar further
$obj->{'myOtherRelatedProp' . 'Key'}->myProp
//desugar final
$obj->myOtherRelatedPropKey->myProp


$obj->{var::($myVar)};//Parse error; ($myVar) evaluates to a literal with the value of $myVar

$obj->{var::$myVar::class};//Parse error; $var::class evaluates to a literal
$obj->{var::($myObj->{$myVar})::class};//Parse error; ($myObj->{$myVar})::class evaluates to a literal
$obj->{var::MyClass::MY_CONSTANT};//Parse error; MyClass::MY_CONSTANT evaluates to a literal

$obj->{(var::MyRelatedClass::${var::MyOtherRelatedClassClass::myOtherRelatedProp . 'Key'})}->myProp//Parse error; The final operator on the right of the inner var:: is concatenation

Possible syntax alternatives

Instead of var::expression, a "function like" notation, i.e. var(expression) could be used, akin to how isset() works. I imagine this may be easier to implement, even if it requires a bunch of the same special casing that isset() currently requires. As long as the keyword appears before the expression, it would result in good DX.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions