rfc:callable-interfaces

This is an old revision of the document!


PHP RFC: Callable interfaces

Introduction

Currently, when you type-hint against “callable” in your method signatures, you can never be certain whether the function will accept the parameters you are giving it, or if the return type is what you'd expect. You can manually validate the return type, and catch exceptions about the invalid parameters, but that is not ideal.

This RFC tries to solve this problem by allowing callables to follow user-specified interfaces. This RFC is inspired by https://wiki.php.net/rfc/typesafe-callable

Proposal

PHP already has a way to define objects that act as functions. That mechanism is the __invoke magic method, which is widely used in libraries and frameworks. In addition to that, Closure already implements __invoke.

__invoke already works quite well: with this proposal, generic *callable* arrays, functions and objects will be usable as if they implemented a matching interface:

interface RegisterUser {
    public function __invoke(Username $username) : UserRegistration;
}

We can now implicitly implement this interface by just defining any *callable* that matches this interface:

As a function:

function register (Username $username) : UserRegistration {
    // ... domain logic here ...
 
    return new UserRegistration($userId);
}

As a closure:

$register = function (Username $username) : UserRegistration {
    // ... domain logic here ...
 
    return new UserRegistration($userId);
};

As a static callable array:

class Register {
    public static function register(Username $username) : UserRegistration {
        // ... domain logic here ...
 
        return new UserRegistration($userId);
    }
}
 
$register = [Register::class, 'register'];

As an instance callable array:

class Register {
    public function register(Username $username) : UserRegistration {
        // ... domain logic here ...
 
        return new UserRegistration($userId);
    }
}
 
$register = [new Register(), 'register'];

We are now able to consume any of these callables wherever the interface is required in a type-hint:

function runRegistration(Username $username, RegisterUser $handler) {
    var_dump($handler($username));
}
 
runRegistration(new Username('DASPRiD'), $register);

In order for this to work, any implicitly defined callable should be cast to a *Closure* at call-time.

In pseudo-code, this would look like following, under the hood:

function passAParameterToAPhpFunction(callable $callable, $expectedParameterInterface) {
    if (! $expectedParameterInterface->isCallableInterface()) {
        passParameter($callable);
 
        return;
    }
 
    if (! $expectedParameterInterface->matches($callable)) {
         throw new TypeError('Expected X, got Y');
    }
 
    if (! is_object($callable)) {
        $callable = wrapInCompatibleAnonymousClass($callable);
    }
 
    passParameter($callable);
}

Still Open for Discussion

How will instanceof behave, when asked for a type-check against callable?

interface RegisterUser {
    public function __invoke(Username $username) : UserRegistration;
}
 
interface DeleteUserRegistration {
    public function __invoke(Username $username) : UserRegistration;
}
 
$register = function (Username $username) : UserRegistration {
    return new UserRegistration(...);
};
 
var_dump($register instanceof DeleteUserRegistration); // true? false? possibly want to keep current semantics here.

Backward Incompatible Changes

This RFC expects no BC breaks.

Proposed PHP Version(s)

7.1

Proposed Voting Choices

This RFC requires a 2/3 majority to pass.

Patches and Tests

Patch will be available before voting commences.

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged to
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
rfc/callable-interfaces.1459944009.txt.gz · Last modified: 2017/09/22 13:28 (external edit)