rfc:forbid_null_this_in_methods

PHP RFC: Forbid $this being null in instance methods

Introduction

Currently it is possible for $this to be NULL inside a non-static method. This RFC proposes to remove this possibility, while still retaining compatibility with legacy PHP 4 code that performs static calls to non-static methods.

The primary case which may result in $this being undefined (with an effective NULL value) occurs when a non-static method is called statically:

// Deprecated: Non-static method A::method() should not be called statically
A::method();
 
class A {
	public function method() {
		// Uncaught Error: Using $this when not in object context
		$this->something();
	}
}

Note that the call throws a deprecation warning, but is allowed. However the use of $this inside the method then throws an Error exception. This RFC essentially proposes to throw the Error exception already when A::method() is called.

A common pattern for legacy PHP 4 code is to statically call non-static methods that do not use $this (simply because PHP 4 had no concept of static methods). Under this RFC these calls continue to be allowed (with the existing deprecation warning). Only calls to methods actually using $this are affected. For example the following remains valid:

// Deprecated: Non-static method A::method() should not be called statically
A::method();
 
class A {
	public function method() {
		// No use of $this in here!
	}
}

The goal of this RFC is to tighten guarantees that any reasonable person is expecting anyway, removing one of our largest remaining type-system loopholes. This RFC will ensure that within a non-static method the assertion $this instanceof self never fails. Furthermore this eliminates a number of hot runtime checks and paves the way for future optimization (like method inlining).

Proposal

This section outlines the precise semantics of the proposal, which covers a slightly larger scope than outlined in the Introduction. In particular this RFC also encompasses rebinding of fake method-closures and also deals with concerns surrounding use of $this in free functions (or other scope-free contexts), which are implementationally related.

Forbid static calls to non-static methods using $this

The primary change of this RFC is to forbid static calls to non-static methods that use $this, while calls to non-static methods not using $this continue to be allowed. The following examples illustrate which calls will be affected and which will behave as currently. The first two examples are repeated from the Introduction.

This A::method() call is prohibited, because A::method() uses $this:

class A {
	public function method() {
		$this->foo();
	}
}
 
// This will throw an Error exception, because A::method() uses $this
A::method();

This A::method() call is allowed, because A::method() does not use $this:

class A {
	public function method() {
		// No use of $this in here
	}
}
 
// This call is allowed and will only throw a deprecation warnings
A::method();

This A::method() call is allowed, because the $this access is realized using variable-variables and cannot (generally) be detected at compile-time:

class A {
	public function method() {
		$name = 'this';
		var_dump($$name);
	}
}
 
// This call is allowed, because the use of $this is not known at compile-time
A::method();

This behavior is similar to many other existing restrictions that can be circumvented using variable-variables. For example assignments to $this are prohibited at compile-time, but this restriction can also be avoided using variable-variables.

The following parent::method() and A::method() calls are allowed, because they are not static calls, but rather scoped instance calls. They will have a valid $this (namely of class B or a child class):

class A {
	public function method() {
		$this->foo();
	}
}
class B extends A {
	public function method() {
		// This call is allowed, because it's not a static call (it only looks like one)
		parent::method();
		// This call is also allowed, for the same reason
		A::method();
	}
}

Forbid unbinding $this from fake closures for methods using $this

Next to static calls to non-static methods there is another way how $this may end up being NULL inside a method, namely closure rebinding of fake closures returned by ReflectionMethod::getClosure():

class A {
	public function method() {
		$this->foo();
	}
}
 
$closure = (new ReflectionMethod('A', 'method'))->getClosure(new A);
$closure = $closure->bindTo(null, 'A');
$closure();

For the case where A::method() uses $this, this operation will now be forbidden. Like all other rebinding failures, it will result in a warning and a false being returned.

Note that this does not affect ordinary closures in any way. Only closures obtained using ReflectionMethod::getClosure() may be affected.

Alternative action: This check could be made more strict, by forbidding unbinding of $this even if the non-static method does not use $this. As we have no legacy concerns here, it may be preferable to be stricter right away.

Remove special treatment of $this in non-scoped contexts

It is currently possible to use $this as an ordinary variable in non-scoped contexts (e.g. free functions), however $this will still be subjected to special treatment in some (but not all) operations. For example:

function test($this) {
	$this->prop = 42;
}
test(new stdClass);

This code will not generate compile-time errors and will also allow the function call, however the $this→prop operation will throw an Error exception with message “Using $this when not in object context”. This is odd in that the $this variable here does hold an object (which a var_dump would show), but the $this->prop access still generates an error.

This RFC will allow the shown code to run without errors.

Alternative action: Alternatively we could completely forbid use of $this in non-scoped contexts. An issue with this is that we would also have to forbid unbinding of $this from closures that use it, which has unclear backwards compatibility implications.

Backward Incompatible Changes

This RFC is specifically designed to retain compatibility with patterns seen in legacy PHP 4 code. Calls are only prohibited if $this is used in the method, as such we generally only generate an error in cases that would already error at a later point in time.

There is only one case I can imagine where this change might cause a genuine compatibility issue, namely code checking isset($this) to determine whether a method was called statically or non-statically:

class A {
	public function test() {
		if (isset($this)) {
			echo "Called non-statically\n";
		} else {
			echo "Called statically\n";
		}
	}
}
(new A)->test(); // Prints: Called non-statically
A::test();       // Prints: Called statically

This pattern will no longer be supported. (Actually it's still possible to do this using variable-variables for people with strong masochistic tendencies.)

Future Scope

In the future static calls to non-static methods may be completely forbidden (as they are already deprecated). However this change is not part of this RFC.

Vote

As this is a language change, a 2/3 majority is required. The vote will be a simple Yes/No vote.

Patches and Tests

Patch: https://github.com/php/php-src/pull/1894

The patch implements the changes described in this proposal and removes a large slew of NULL checks from the Zend VM.

rfc/forbid_null_this_in_methods.txt · Last modified: 2017/09/22 13:28 (external edit)