ringmaster
ringmaster

Reputation: 3359

How can I use PHPDoc to type-hint the parameters of a Callable?

I have a method that accepts a callback as a parameter. I would like to provide a signature in the PHPDoc for the class method that outlines the parameters for the callback function to be passed to that method so that my IDE (PHPStorm) can produce valid type hints for functions that are passed to my method, or at least someone looking at the code can determine the signature of the callback they're intended to provide.

For example:

class Foo {
  public $items = [];
  /**
  * @param Callable(
  *   @param ArrayObject $items The list of items that bar() will return
  * ) $baz A callback to receive the items
  **/
  public function bar(Callable $baz) {
    $items = new ArrayObject($this->items);
    $baz($items);
  }
}

The method bar has one parameter, $baz, which is a callback function. Any function passed as an parameter to bar() must accept an ArrayObject as its only parameter.

Ideally, it should be possible to include multiple parameters for the Callable, just like for any other method.

When I write the following code:

$foo = new Foo();
$foo->bar(function(

...I should then receive a parameter list that correctly hints the type (ArrayObject) of the accepted parameter for this function call.

Is such a thing possible? Does PHPStorm or another IDE support it? Is there a recommended/standard way of documenting this even if there is no IDE support?

Upvotes: 51

Views: 19746

Answers (5)

ZhukV
ZhukV

Reputation: 3178

It's possible to use callable and \Closure type hint.

As an example:

/**
 * @param callable(int, string): int $callable
 *
 * @return int
 */
function some(callable $callable): int
{
    return $callable(1, 'foo-bar');
}

/**
 * @param Closure(\stdClass, array): void  $closure
 *
 * @return mixed
 */
function some2(\Closure $closure): int
{
    return $closure((object) [], []);
}

Attention: it works only if you provide return typehint:

/**
 * @param callable(): int $callable - Work
 * @param callable: int $callable - Don't work (missed arguments)
 * @param callable(int, int, string) - Don't work (missed return type hint)
 * @param callable(int, int, SomeClass): void - Work
 */

PhpStorm and static analyzers (phpstan for example) fine correct read this notations.

Upvotes: 23

raveren
raveren

Reputation: 18542

See the example below, and note you can optionally provide names for the parameters to document them in the same place.

Good naming is better than excellent comments!

/**
 * Use whichever:
 * @param callable(int $status, User[] $usersToUpdate): int $a
 * @param \Closure(int $status, User[] $usersToUpdate): int $b
 */
function acceptsCallbacks(callable $a, \Closure$b): void
{
    $a(1, []);
    // ...
}

The difference between the two, by the way, is that a Closure must be an anonymous function, where callable also can be a normal function.


This type of annotation is accepted by PHPStorm and PHPstan!:

PHPstorm annotation example

Upvotes: 1

rcd
rcd

Reputation: 4290

PHP 7+:

Using an interface for the callable combined with anonymous classes will do the trick. It's not very handy and leads to bit too complex code for the class-consumer, but currently it's the best solution in terms of static code-analysis.

/**
 * Interface MyCallableInterface
 */
interface MyCallableInterface{
    /**
     * @param Bar $bar
     *
     * @return Bar
     */
    public function __invoke(Bar $bar): Bar;
}

/**
 * Class Bar
 */
class Bar{
    /**
     * @var mixed
     */
    public $data = null;
}

/**
 * Class Foo
 */
class Foo{
    /**
     * @var Bar
     */
    private $bar = null;

    /**
     * @param MyCallableInterface $fn
     *
     * @return Foo
     */
    public function fooBar(MyCallableInterface $fn): Foo{
        $this->bar = $fn(new Bar);
        return $this;
    }
}

/**
 * Usage
 */
(new Foo)->fooBar(new class implements MyCallableInterface{
    public function __invoke(Bar $bar): Bar{
        $bar->data = [1, 2, 3];
        return $bar;
    }
});

If you're using PhpStorm it will even auto-generate the __invoke-Method's signature & body within the anonymous class.

Upvotes: 18

Percutio
Percutio

Reputation: 954

I overcame this problem by defining a static function within the class using the callable. That function has its own doc-block and I just refer to it in the method that's requiring my callable using PHPDoc's @see tag.

class Foo
{
    /**
     * Description of the "bar" callable. Used by {@see baz()}.
     *
     * @param int $index A 1-based integer.
     * @param string $name A non-empty string.
     * @return bool
     * @see baz()
     * @throws \Exception This is a prototype; not meant to be called directly.
     */
    public static barCallable($index, $name)
    {
        throw new \Exception("barCallable prototype called");
    }

    /**
     * Description of the baz() method, using a {@see barCallable()}.
     *
     * @param callable $bar A non-null {@see barCallable()}.
     * @see barCallable()
     */
    public function baz(callable $bar)
    {
        // ...
        call_user_func($bar, 1, true);
        // ...
    }
}

This works well in PhpStorm 10. the Quick Documentation allows to navigate from the method documentation to the prototype documentation easily.

I make my prototype function throw an exception to make it clear it's not meant to be called. I could use a protected or private scope, but then PHPDoc would not always pick the doc-block for documentation generation.

Unfortunately PhpStorm can't track usage of callbacks. It doesn't provide Parameter Info either when using the method requiring a callback, but the callback is at least formally documented.

This approach also has the added benefit of validating a callback definition from the reflection of the prototype at run-time.

Upvotes: 4

ualinker
ualinker

Reputation: 745

It is not possible in PhpStorm by now. I can't even think of other solution which do relatively the same by other means.

Upvotes: 3

Related Questions