[php-src] Issue #9449: Equivalent to "::class" for obtaining property and variable names as a string
| From: | boenrobot | Date: | Tue, 30 Aug 2022 14:39:13 +0000 |
| Subject: | [php-src] Issue #9449: Equivalent to "::class" for obtaining property and variable names as a string | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-242339@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/9449
Author: 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](https://phpstan.org/writing-php-code/phpdoc-types#class-string)).
### 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](https://phpstan.org/writing-php-code/phpdoc-types#callables)).
### 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``.
# 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.
```php
$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
```
# 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.
# 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.
```php
//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
```