4

Step by step, I am migrating my project(s) from PHP 7.1 to PHP 8.0.

In the official PHP manual, in the subchapter "Deprecated Features" of the chapter "Migrating from PHP 7.3.x to PHP 7.4.x", I tried to understand the following deprecation description:

Unbinding $this when $this is used

Unbinding $this of a non-static closure that uses $this is deprecated.

Though, without success.

That's why I would appreciate, if someone could explain me in detail, what this means. Maybe a code snippet could be helpful too.

Thank you very much for your time!


Maybe this helps for the explanation too:

In my project, I have only one situation to which, in my opinion, this deprecation notice could be appliable: to the method executeGroupHandler of the RouteCollection class presented bellow. I preferred to paste a bit more code of the class, in order to help you understand the context in which I use the method executeGroupHandler.

RouteCollection class:

<?php

namespace Packages\Router;

//...
use Packages\Router\RouteCollectionInterface;


/**
 * Route collection.
 */
class RouteCollection implements RouteCollectionInterface {

    //...

    /**
     * Group patterns list. Indexed array.
     *
     * Each time a group handler is executed its pattern is saved in this list.
     * All addRoute operations taken place inside the scope of a group handler
     * prefix the pattern of the corresponding route with the saved group pattern.
     *
     * @var array
     */
    private $groupPatterns = [];

    //...

    /**
     * Add a group (helper method).
     *
     * @param string $pattern Group pattern.
     * @param \Closure $handler Group handler.
     * @return $this
     */
    public function group(string $pattern, \Closure $handler) {
        $this->addGroup($pattern, $handler);
        return $this;
    }

    /**
     * Add a group.
     *
     * @param string $pattern Group pattern.
     * @param \Closure $handler Group handler.
     * @return $this
     */
    private function addGroup(string $pattern, \Closure $handler) {
        $this->saveGroupPattern($pattern);

        $this->executeGroupHandler($handler);

        /*
         * Remove the last group pattern from the group patterns list. This step
         * is performed only after all calls for adding groups/routes inside the
         * scope of the current group handler have finished their processing.
         */
        $this->popLastGroupPattern();

        return $this;
    }

    /**
     * Save a group pattern.
     *
     * @param string $pattern Group pattern.
     * @return $this
     */
    private function saveGroupPattern(string $pattern) {
        $this->groupPatterns[] = $pattern;
        return $this;
    }

    /**
     * Execute a group handler.
     *
     * Temporarily bind the group handler to the route collection 
     * object - defined by the argument in Closure::call - and 
     * execute it. Inside the scope of the group handler, the route 
     * collection will be accessed using the keyword "$this".
     * 
     * @link https://www.php.net/manual/en/closure.call.php Closure::call
     *
     * @param \Closure $handler Group handler.
     * @return mixed The return value of calling the handler.
     */
    private function executeGroupHandler(\Closure $handler) {
        return $handler->call($this);
    }

    /**
     * Pop the group pattern off the end of group patterns list.
     *
     * @return string The popped group pattern.
     */
    private function popLastGroupPattern() {
        return array_pop($this->groupPatterns);
    }

}

Use of RouteCollection class:

Having the RouteCollection class defined, I use it similar to the following:

<?php

use Packages\Router\RouteCollection;
use SampleMvc\App\View\Template\Users\AddUser as AddUserView;
use SampleMvc\App\Controller\Users\AddUser as AddUserController;

$routeCollection = new RouteCollection();

// Add a group of routes to the route collection.
$routeCollection->group('/users/add', function() {
    $this->get('', [AddUserView::class, 'index']);

    $this->post('', [
        'controller' => AddUserController::class,
        'view' => [AddUserView::class, 'addUser'],
    ]);
});

//...
4

2 回答 2

3

The deprecation was proposed (and accepted) in this RFC which gives some more details about what is deprecated and why.

The last sentence explains which closures are affected:

In particular this applies to non-static closures declared inside non-static methods. A $this binding can be avoided in the first place by marking the closure as static.

This was later narrowed down further in this commit, so that it only applies to closures where $this is actually mentioned in the closure.

The first sentence, meanwhile, gives a clearer example of what is being deprecated:

Currently it is possible to unbind the $this variable from a closure that originally had one by using $closure->bindTo(null).

The key words being unbind rather than rebind, and the null in the example.

There's some more background from Nikita in this comment:

The reason we're interested in this deprecation is exclusively to enable some performance improvements in PHP 8 related to $this accesses, which is enabled by the removal of static calls to non-static methods. $this accesses can be split into two categories: Those where we know $this to be non-null and those where we don't. Method calls (will) fall into the former category. With this deprecation closures will also fall into the former category.

In other words, in PHP 8, the engine will blindly assume that any reference to $this in a closure will actually be an object, not null.

So the specific scenario that's deprecated is where you have a closure that mentions $this, and you later unbind it, so that $this isn't set to anything at all. As long as you're providing a new value for $this, you should be unaffected, because $this will never be null.

于 2021-02-03T17:41:46.623 回答
1

Good quiestion, I don't understand what "unbinding variable of a closure" means in PHP, too.

I've wrote a simple test to check if binding another $this can be considered "unbinding $this":

class ClosureTest extends TestCase
{

    private $x = 1;

    public function testClosure(): void
    {
        var_dump(\PHP_VERSION);
        $callable = function (): int {
            return $this->x;
        };
        self::assertSame(1, \call_user_func($callable));

        $closure = \Closure::fromCallable($callable);

        $obj = new class {
            private $x = 2;
        };
        self::assertSame(2, $closure->call($obj));
    }
}

I've run it both under 7.3 and 8.0 and didn't got any notices, so probably your code is safe to run under 8.0, too. But I will investigate the question further.

于 2021-02-03T17:24:20.283 回答