====== PHP RFC: Explicit $this parameter ======
* Version: 1.0
* Date: 2026-02-08
* Author: Tim Düsterhus (tim@tideways-gmbh.com), Arnaud Le Blanc (arnaud.lb@gmail.com)
* Status: Draft
* Implementation: tbd
===== Introduction =====
The elevator pitch for the RFC. The first paragraph of this section will be rendered slightly larger to give it emphasis.
Please write an introduction that helps people by providing concise context and include a representative small code snippet.
===== Proposal =====
We propose allowing a named parameter this: to be combined with the “scope resolution operator” syntax of referring to a method to be used as an alternative way of calling an instance method. The value passed to the this: parameter will effectively be used as the left side of the -> operator:
Since the this: parameter does not refer to a parameter in the underlying argument list and thus there is no ambiguity with regard to positional parameters it may be placed at any position of the parameter list. In particular it may appear at the first position, before all positional parameters, mirroring the left-most position for a regular method call.
$now = new DateTimeImmutable();
$formatted = DateTimeImmutable::format(this: $now, "c");
// is effectively equivalent to
$formatted = $now->format('c');
==== Liskov Substitution Principle ====
The semantics follow that of a “wrapper function” copying all parameter names, parameter types and the return type of the method specified in the method reference. The parameter type of the this: parameter is the class name that is specified in the method reference. If the method is overridden in a child class, the overridden method will be called, not the method that is specified.
This also means that it is legal to specify an interface method and provide any object implementing the interface.
$formatted = DateTimeInterface::format(this: new DateTimeImmutable(), "c");
Since the semantics follow that of a “wrapper function”, the signature does not adapt to the object that is actually passed. If a parameter is widened in a child class only the original types remain valid. If a parameter is renamed, the parameter name of the specified method must be used.
interface MyInterface {
public function method(string $foo): int|string|bool;
}
class ParentClass implements MyInterface {
public function method(string|array $foo): string {
return 'parent';
}
}
class ChildClass extends ParentClass {
public function method(string|array $bar): string {
return 'child';
}
}
// Throws TypeError, since an array is not a valid parameter on the interface,
// despite being valid on ChildClass::method().
MyInterface::method(this: new ChildClass(), []);
// Throws an Error about an unknown parameter 'bar', because the parameter
// is called 'foo' on ParentClass::method().
ParentClass::method(this: new ChildClass(), bar: []);
// Calls ChildClass::method().
var_dump(ParentClass::method(this: new ChildClass(), foo: 'bar')); // string(5) "child"
It is however not allowed to provide an object that is not contravariant with the specified class name, even if it implements a method with a compatible signature:
final class NotDateTimeImmutable {
public function format(string $format): string { }
}
// Throws a TypeError.
$formatted = DateTimeImmutable::format(this: new NotDateTimeImmutable(), "c");
=== Default Values ===
Default values are taken from the method that is actually being executed, not the method that is specified in the method reference. There are two reasons for these semantics:
- The “child method” could define additional parameters where no default value is available within the “parent method”.
- Consistency with the semantics of partial function application.
class ParentClass {
public function dump(string $x = 'ParentClass') {
var_dump($x);
}
}
class ChildClass extends ParentClass {
public function dump(string $x = 'ChildClass', string $y = 'extra parameter') {
parent::dump($x);
var_dump($y);
}
}
ParentClass::dump(this: new ChildClass());
// string(10) "ChildClass"
// string(15) "extra parameter"
=== From Within The Class ===
There is one exception from the rule that the child class' method will be called if it is overridden. For a call ClassName::method(this: $object) if:
- The referenced class name ClassName matches that of a parent (or grandparent, grandgrandparent, …) class of the current class (is_subclass_of(self::class, ClassName::class)).
- The object $object is an instance of the current class ($object instanceof self).
The method() defined in ClassName will be called, not $object->method().
class GrandparentClass {
public function method() {
echo "Grandparent";
}
}
class ParentClass extends GrandparentClass {
public function method() {
echo "Parent";
}
}
class Example extends ParentClass {
public function method() {
echo "Example";
}
public function sameClass() {
// Existing behavior: Echos "Grandparent".
GrandparentClass::method();
// this: parameter with $this as value. Also echos "Grandparent",
// mirroring the existing behavior, but making it clear that
// GrandparentClass::method() is not a static method call by
// explicitly providing $this.
GrandparentClass::method(this: $this);
// The same happens when specifying a different object instance
// of MyClass. This also echos "Grandparent".
GrandparentClass::method(this: new MyClass());
}
public function childClass() {
// Existing behavior: Echos "Grandparent".
(new ChildClass())->bypassOverridden();
// this: parameter with an instance of ChildClass as value.
// Also echos "Grandparent", mirroring the existing capabilities.
GrandparentClass::method(this: new ChildClass());
}
private function bypassOverridden() {
GrandparentClass::method();
}
public function parentClass() {
// It however does not happen when specifying an object instance
// of a parent class.
// This echos "Parent".
GrandparentClass::method(this: new ParentClass());
}
}
class ChildClass extends Example {
public function method() {
echo "Child";
}
}
==== Dynamic Parameter Lists ====
For the this: parameter to be recognized it needs to be explicitly specified. Dynamically passing it using argument-unpacking of an array or Traversable with a key 'this' will not result in an instance call. The same applies for dynamic calls using call_user_func() and call_user_func_array(). There are two reasons for this:
- The decision between an instance call or a static call should not be made at runtime.
- Enabling a 'this' parameter name to be passed into a variadic parameter.
final class Example {
public function instanceCall(...$args) {
var_dump($args);
}
public static function staticCall(...$args) {
var_dump($args);
}
}
Example::instanceCall(this: new Example(), ...['this' => 'this', 'foo' => 'bar']);
// array(2) {
// ["this"]=>
// string(4) "this"
// ["foo"]=>
// string(3) "bar"
// }
Example::staticCall(...['this' => new Example(), 'foo' => 'bar']);
// array(2) {
// ["this"]=>
// object(Example)#1 (0) {
// }
// ["foo"]=>
// string(3) "bar"
// }
call_user_func_array(['Example', 'staticCall'], ['this' => new Example(), 'foo' => 'bar']);
// array(2) {
// ["this"]=>
// object(Example)#1 (0) {
// }
// ["foo"]=>
// string(3) "bar"
// }
==== __call() ====
Virtual methods using __call() are supported as expected.
final class Example {
public function __call(string $name, array $arguments) {
var_dump($name);
}
}
Example::magic(this: new Example()); // string(5) "magic"
==== Static Methods and Free-standing Functions ====
Free-standing functions however may not be called with this:.
function example() { }
example(this: new stdClass()); // Not allowed.
Although not particularly useful, static methods are supported, similarly to how it is legal to call static methods using the -> operator. The static::class value matches the class of the passed object:
class ParentClass {
public static function staticMethod() {
var_dump(static::class);
}
}
class ChildClass extends ParentClass { }
ParentClass::staticMethod(this: new ChildClass()); // string(10) "ChildClass"
It remains illegal to define a parameter called $this:
final class Example {
public static function staticMethod(
$this, // Not allowed.
) { }
}
==== Partial Function Application ====
The this: parameter may be combined with partial function application to create a partial function where the “object instance” will be filled in at call time. Specifying the this: parameter outside the left-most position is particularly useful for partial function application, since it allows to specify the position of the object instance parameter relative to regular parameters. If a variadic placeholder is specified it must come last.
$c = DateTimeImmutable::format(this: ?, "c");
$c = DateTimeImmutable::format(this: ?, format: "c");
// are both effectively equivalent to:
$c = static fn (DateTimeImmutable $__this): string
=> $__this->format('c');
$c = DateTimeImmutable::setTimestamp(this: ?, ?);
$c = DateTimeImmutable::setTimestamp(this: ?, timestamp: ?);
// are both effectively equivalent to:
$c = static fn (DateTimeImmutable $__this, int $timestamp): DateTimeImmutable
=> $__this->setTimestamp($timestamp);
// and
$c = DateTimeImmutable::setTimestamp(this: ?, ...);
// is effectively equivalent to:
$c = static fn (DateTimeImmutable $__this, int $timestamp): DateTimeImmutable
=> $__this->setTimestamp($timestamp, ...array_slice(func_get_args(), 2));
// but
$c = DateTimeImmutable::setTimestamp(?, this: ?);
$c = DateTimeImmutable::setTimestamp(timestamp: ?, this: ?);
// are both effectively equivalent to:
$c = static fn (int $timestamp, DateTimeImmutable $__this): DateTimeImmutable
=> $__this->setTimestamp($timestamp);
If the this: parameter is placed after optional parameters, they will become required.
$c = DateTimeImmutable::setTime(this: ?, ?, ?, ?, ?);
// is effectively equivalent to:
$c = static fn (DateTimeImmutable $__this, int $hour, int $minute, int $second = 0, int $microsecond = 0): DateTimeImmutable
=> $__this->setTime($hour, $minute, $second, $microsecond);
// but
$c = DateTimeImmutable::setTime(?, ?, ?, ?, this: ?);
// is effectively equivalent to:
$c = static fn (int $hour, int $minute, int $second, int $microsecond, DateTimeImmutable $__this): DateTimeImmutable
=> $__this->setTime($hour, $minute, $second, $microsecond);
=== From Within The Class ===
The method lookup rules for direct calls apply equally to partial function application. The self at the time of the PFA declaration is relevant.
class GrandparentClass {
public function method() {
return "Grandparent";
}
}
class ParentClass extends GrandparentClass {
public function method() {
return "Parent";
}
}
class Example extends ParentClass {
public function method() {
return "Example";
}
public static function run($objects) {
return array_map(GrandparentClass::method(this: ?), $objects);
}
public static function runWithPfa($pfa, $objects) {
return array_map($pfa, $objects);
}
public static function getPfa() {
return GrandparentClass::method(this: ?);
}
}
class ChildClass extends Example {
public function method() {
return "Child";
}
}
$objects = [
new GrandparentClass(),
new ParentClass(),
new Example(),
new ChildClass(),
];
// PFA is defined and called within Example.
// Echos "Grandparent, Parent, Grandparent, Grandparent"
echo implode(", ", Example::run($objects));
// PFA is defined within Example and called within the global scope.
// Echos "Grandparent, Parent, Grandparent, Grandparent"
echo implode(", ", array_map(Example::getPfa(), $objects));
// PFA is defined and called within the global scope.
// Echos "Grandparent, Parent, Example, Child"
echo implode(", ", array_map(GrandparentClass::method(this: ?), $objects));
// PFA is defined within the global scope and called within Example
// Echos "Grandparent, Parent, Example, Child"
echo implode(", ", Example::runWithPfa(GrandparentClass::method(this: ?), $objects));
==== Examples ====
There are two main use cases. The primary use case is enabling partial function application for the object instance:
// Partially applying $this for the 'choice_label' with Symfony Form’s ChoiceType
// https://symfony.com/doc/current/reference/forms/types/choice.html#advanced-example-with-objects
$builder->add('category', ChoiceType::class, [
'placeholder' => false,
'choices' => [
new Category('Cat1'),
new Category('Cat2'),
new Category('Cat3'),
new Category('Cat4'),
],
'choice_label' => Category::getName(this: ?),
/* … */
]);
The other is making calls to grandparent methods explicit:
===== Backward Incompatible Changes =====
While $this is a reserved variable name, thus is guaranteed to never be a valid parameter name, it may nevertheless be used as a named parameter name to use as a key for a variadic parameter:
string(4) "this" }
If this RFC is accepted, this would fail with a clear compile-time error for non-methods. For methods it will fail at runtime, unless the value passed to this: happens to be of the correct type. Users are still able to use "this" as a key in the resulting array by using the array unpacking syntax to pass parameters:
dump(...['this' => 'test']);
Seifeddine Gmati scanned 18975 Packagist packages containing 626044 files with Mago without finding any usage of this: as a named parameter. For reference, self: also had zero usages, object: had 32 and static: had 6.
Given that the backwards compatibility break has a direct workaround that does not require complex tooling combined with the non-existent usage in public libraries, we expect the impact to be minimal in practice.
===== Proposed PHP Version(s) =====
Next PHP 8.x (8.6).
===== RFC Impact =====
==== To the Ecosystem ====
IDEs and Static Analyzers need to understand the special semantics of the this: parameter to provide proper diagnostics:
* It does not refer to an actual parameter, but to the $this value in an instance call
* It may appear before positional parameters, despite being a named parameter.
It does now introduce any new syntax, thus files making use of the feature continue to parse correctly and incorrect diagnostics are localized to the calls making use of the feature.
==== To Existing Extensions ====
None.
==== To SAPIs ====
None.
===== Open Issues =====
None.
===== Future Scope =====
This section should outline areas that you are not planning to work on in the scope of this RFC, but that might be iterated upon in the future by yourself or another contributor.
This helps with long-term planning and ensuring this RFC does not prevent future work.
===== Voting Choices =====
Primary Vote requiring a 2/3 majority to accept the RFC:
* Yes
* No
* Abstain
===== Patches and Tests =====
tbd
===== Implementation =====
After the RFC 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
===== References =====
* [[partial_function_application_this|Partial Function Application for instance of non-static methods ("$this")]]: A precursor RFC to this one.
===== Rejected Features =====
None.
===== Changelog =====
* 2026-02-16: Initial version