rfc:complete_callstatc_magic

PHP RFC: Invoke __callStatic when non-static public methods are called statically

Introduction

The _ _callStatic magic method should be invoked when non-static public methods are called in a static context.

In PHP manual, _ _callStatic is described like below.

__callStatic() is triggered when invoking inaccessible methods in a static context.

In this context, interpreting inaccessible merely as invisible is insufficient because it omits whether the call is being made statically. It would be clearer to interpret it as not callable in a static context.

Non-static public methods are visible but cannot be called statically. Therefore, instead of throwing an error, the _ _callStatic method should be invoked when attempting to call a public method statically.

Here are examples of the current code and how the code would look if this RFC is accepted.

Case 1

<?php
namespace App\Models;
 
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    public function scopeActive(Builder $query): void
    {
        $query->where('active', 1);
    }
}
 
$users = User::active()->get();

This is an example of a local scope using Laravel Eloquent ORM.

This code has the advantage of being able to categorize scope methods. However, the IDE cannot find active method, and the Go to Definition feature cannot be used.

Somewhere in the internal code not in _ _callStatic, there is code that matches the active method to the scopeActive method, but it's hidden, not easy to find.

<?php
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
    public function active(): static
    {
        $this->query()->where('active', 1);
 
        return $this;
    }
}
 
$users = User::active()->get();

This code is very clear, aside from the fact that the method is not static.

The IDE recognizes active methods well and the Go to definition feature also works properly.

Case 2

Let's try to create a simple router class similar to the one in Laravel.

<?php
 
class Router
{
    public static __callStatic($method, $args)
    {
        return (new RealRouter())->$method(...$args);
    }
}
 
class RealRouter
{
    public function prefix(): static
    {
        // some code
 
        return $this;
    }
    public function group(): static {}
    ...
} 
 
Router::prefix()->group();

Even if there are only a few core methods, it cannot be made into a single file.

In this case as well, the IDE cannot find prefix method, and the Go to Definition feature cannot be used.

Certainly, you can add PHPDoc to the router class to make it recognize prefix methods, but still, you cannot directly move to the original method using the go to definition feature.

There is another problem: the result of calling a prefix method changes from the Router class to the RealRouter class. Of course, it can be adjusted in _ _callStatic, but it's confusing.

<?php
 
class Router
{
    public static __callStatic($method, $args)
    {
        return (new static())->$method(...$args);
    }
 
    public function prefix(): static
    {
        // some code
 
        return $this;
    }
    public function group() {}
    ...
}
 
Router::prefix()->group();

Aside from the fact that the method is not static, this code is very clear too.

Case 3

Let's make a custom facade in Laravel.

<?php
// 1. app/Services/CustomService.php
namespace App\Services;
 
class CustomService
{
    public function someMethod() {}
}
<?php
// 2. app/Facades/CustomFacade.php
namespace App\Facades;
 
use Illuminate\Support\Facades\Facade;
 
class CustomFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'custom';
    }
}
<?php
// 3. app/Providers/AppServiceProvider.php
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use App\Services\CustomService;
 
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('custom', function () {
            return new CustomService();
        });
    }
}
<?php
use App\Facades\CustomFacade;
 
CustomFacade::someMethod();

Now what if this RFC is accepted?

<?php
// 1. app/Services/CustomService.php
namespace App\Services;
 
class CustomService
{
    public static __callStatic($method, $args)
    {
        return (new static())->$method(...$args);
    }
 
    public function someMethod() {}
}
<?php
use App\Services\CustomService;
 
CustomService::someMethod();

That's it.


When I raised this issue, many people said that the code would be unclear and obscure. But in reality, it's the opposite. As can be seen in the above examples, the code becomes clearer, and navigation through the IDE works much better.

Of course, calling non-static methods in a static-like manner can be confusing, but in modern PHP, it has already become common practice.

I believe this change allows for easier and more flexible use by users (anonymous programmers using the PHP language).

Proposal

The proposal is simple.

Instead of throwing an error when a non-static public method is called statically, the _ _callStatic method should be invoked.

<?php
class MyClas
{
    public static function __callStatic($method, $args)
    {
        echo '__callStatic : ' . $method . " is called statically.\n";
    }
 
    public function nonStaticPublicMethod() {}
}
 
MyClass::nonStaticPublicMethod();

Before

Fatal error: Uncaught Error: Non-static method MyClass::publicMethod() cannot be called statically in ...

After

__callStatic : nonStaticPublicMethod is called statically.

Backward Incompatible Changes

None

Proposed PHP Version(s)

PHP 8.4

Open Issues

None

Patches and Tests

Not yet. Need volunteer.

References

rfc/complete_callstatc_magic.txt · Last modified: 2024/03/29 04:32 by daddyofsky