rfc:nameof

PHP 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

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. 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.

rfc/nameof.txt · Last modified: 2024/01/14 22:49 by withinboredom