====== PHP RFC: NameOf ======
* Date: 2023-05-07
* Author: Robert Landers, landers.robert@gmail.com
* Status: Under Discussion
* First Published at: http://wiki.php.net/rfc/nameof
===== Introduction =====
During PHP execution, it is possible to reflect on most of the code currently running. For example, it is easy to get the name of a class and using reflection utilities: names of properties, methods, and so on. However, getting the name of a callable passed via ''(...)'' is complicated, even though the engine knows it. Getting the name of a constant not bound to a class or a simple variable, is virtually impossible. This RFC proposes a ''nameof'' operator that makes it relatively easy to get the name of virtually any identifier passed to it.
This can be used in attributes:
#[SerializeWith(nameof(Person->serialize))]
class Person {
public function serialize() { /* do stuff */ }
}
In error messages:
function divide(float $numerator, float $denominator): float {
if($denominator === 0) throw new InvalidArgumentException(nameof($denominator) . ' is zero');
}
In match statements:
match($propertyName) {
nameof(MyClass->property) => $value,
nameof(MyClass->otherProperty) => $otherValue,
}
Or anywhere a literal string can be used:
$arr[nameof(myfunc(...))] = true;
As named parameters become more prevalent, providing the end user with the name of the parameter is more important than ever, in a way that it is easy to locate when refactoring, especially as part of a deprecation strategy:
function myAwesomeFunc(mixed $param1 = null, int $param2 = 0) {
// potentially many lines later...
if(is_called_with_deprecated_value($oldParam)) {
$logger->warn(nameof($oldParam) . ' is deprecated in the next version, please use ' . nameof($newParam);
}
}
Here are some further examples showing how the identifier is transformed:
echo nameof($variable); // variable
echo nameof(MyClass->property); // property
echo nameof(MyClass::staticProperty); // staticProperty
echo nameof(Enum::Case); // Case
echo nameof(Object::Const); // Const
echo nameof(myFunction(...)); // myFunction
echo nameof(\MyNamespace\myFunction(...)); // \MyNamespace\myFunction
echo nameof(MY_CONSTANT); // MY_CONSTANT
It's important to note that some things have an ambiguous name that is a compile error:
echo nameof($a[$b]); // is it a or b? (can call nameof($a) or nameof($b) instead)
echo nameof($a === $b); // ??
echo nameof($a($b)); // same as array access
echo nameof($$a); // ask for $a or use the value of $a
echo nameof($a->prop); // what is $a?
These will generate a compile error: "Cannot get the name of an ambiguous expression."
Note that only the right-most part of the identifier is translated to a string. This is because this is almost always what you want. For example, in the case of ''nameof(MyClass->property)'', you are most likely not interested in the name of ''MyClass'' (otherwise, you'd ask for the ''MyClass::class''), but you are interested in the name of ''property'' because that is what you asked for. The key difference between writing ''"property"'' and ''nameof(MyClass->property)'' comes down to humans reading and modifying the code. It is much safer to refactor code when you know the pedigree of a string that should also be refactored.
To continue the ''"property"'' example, it isn't clear what object "property" comes from without digging into the code, perhaps requiring debugging running code. When it is clear that it comes from ''MyClass'', you can know from whence it came. You can also grep the codebase for "MyClass->property" and locate all usages.
===== Proposal =====
A ''nameof'' operator will be introduced into PHP. During compilation, it will validate that all parts of the identifier exist, and if they do not, a warning will be emitted. The right-most identifier will be turned into a string literal.
There is a special case for objects:
1. A ''::'' will denote a static property/method.
2. A ''->'' will denote an instanced property/method
3. The left-hand side must be the type name; it can be an abstract type or an interface.
4. The left-hand side cannot be a variable.
If the first two rules are broken (e.g., a ''::'' where a ''->'' should be used), a warning will be emitted: "name of a static property/method is asked for, using instanced syntax." If the 3rd rule is broken (e.g., a type cannot be found), then the standard error will be emitted for a missing type, and if the 4th rule is broken, a compilation error will be emitted: "cannot get the name of a variables property/method." The exceptions for rule 4 are ''$this'', ''static::'', and ''self::'', which may be used on the left-hand side in appropriate contexts.
There are a limited number of expressions that can resolve to a name using ''nameof()'':
* variables (but not variable-variables): ''nameof($var)'' resolves to 'var'
* properties: ''nameof($this->prop)'', ''nameof(MyClass->prop)'' resolves to 'prop'
* first-class callables: ''nameof(strlen(...))'' resolves to 'strlen'
* static properties and constants: ''nameof(A::Prop)'' resolves to 'Prop'
* constants: ''nameof(MY_CONST)'' resolves to 'MY_CONST'
The name will always be fully qualified, with a beginning slash, if it is a first-class callable or constant and it refers to a globally accessible identifier. For example:
namespace Example;
class A {
public function doStuff(): void {}
}
function doStuff(): void {}
echo nameof(doStuff(...)); // \Example\doStuff
echo nameof(A->doStuff(...)); // doStuff
Additionally, consider traits and aliasing:
trait A {
public function example() {}
}
trait B {
public function example() {}
}
class C {
use A, B {
A::example insteadof B;
B::example as exampleB;
}
}
echo nameof(C->exampleB(...)); // outputs: "exampleB"
echo nameof(B->example(...)); // outputs: "example"
This adheres to the basic refactoring safety mentioned earlier, since refactoring ''B->example()'' will show up in ''C'', which will then be found by looking for ''C->exampleB'', if it needs to be refactored at all. If ''nameof(C->exampleB)'' returned 'example,' other code may accidentally call ''C->example()'', which would not be ideal, not to mention counter-intuitive.
===== Backward Incompatible Changes =====
There are no backward incompatible changes at this time except that `nameof` will become a reserved word.
===== Future Scope =====
This could be expanded in the future to allow classes and types. For example, ''nameof(int)'' or ''nameof(MyClass)'' but can already be supported via the silence operator.
===== Proposed PHP Version(s) =====
* 8.4: release
* 8.3.x: ''\nameof'' becomes a reserved word, emitting a deprecation warning.
===== RFC Impact =====
==== To SAPIs ====
Should be none
==== To Existing Extensions ====
Will existing extensions be affected? No
==== To Opcache ====
a new AST node (similar to isset) will need to be handled.
==== New Constants ====
None
===== Open Issues =====
===== Unaffected PHP Functionality =====
PHP will largely be unaffected by this change.
===== Future Scope =====
None.
===== Proposed Voting Choices =====
This is a simple yes-or-no vote to include this feature. 2/3 majority required to pass.
===== Patches and Tests =====
experimental implementation: https://github.com/php/php-src/pull/11172/files (to be updated)
===== Implementation =====
After the project is implemented, this section should contain
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
- a link to the language specification section (if any)
===== References =====
- internals discussion: https://externals.io/message/120256
===== Rejected Features =====
Classes need not be supported by ''nameof()'' because there is already an easy way to get the class via ''::class''. This would also be ambiguous when a class name matches a constant name.
There were some suggestions to use ''::name'' instead of ''nameof''; however, this introduces some ambiguity. Take, for example:
class A {
public const name = 'bob';
}
function example(): A {
return new A();
}
echo example()::name;
echo A::name;
The author believes ''name'' is a rather popular name for variables and would inevitably confuse things. In the example above, "bob" is output, but it looks like we could be getting the name of ''example'' if we don't look hard enough. Further, it prevents us from getting the names of constants:
const A = 'billy';
class A {
public const name = 'bob';
}
echo A::name;
Are we getting the string "A" or "bob" in this example? As it is currently, we would output "bob," and the author feels rather strongly that this is correct. Thus, something like ''::name'' is rejected.