Sitelet https://github.com/phpstan/phpstan/issues/8214
Skip to content

callable parameter with any number of arguments #8214

Description

@ondrejmirtes

Feature request

From #6813

I recently tried to create something where this would have been quite useful: a higher-order function to take functions that throw exceptions, catch (and log) them, and then return a WP_Error object instead.

/**
 * Wraps a function so that, when run, any exceptions are caught, logged, and converted to a generic WP_Error.
 *
 * Works with functions of up to 5 parameters. (Add more PX generic types to this docblock to add support for more params.)
 *
 * @template P1
 * @template P2
 * @template P3
 * @template P4
 * @template P5
 * @template R
 *
 * @param (Closure(P1, P2, P3, P4, P5): R) $fn Function that could throw.
 *
 * @return (Closure(P1, P2, P3, P4, P5): (R|WP_Error))
 */
function handleExceptionsAsWpErrors(Closure $fn, LoggerInterface $logger): Closure {
	return static function (...$params) use ($fn, $logger) {
		try {
			return $fn(...$params);
		} catch (Throwable $e) {
			$logger->error($e->getMessage());
			return new WP_Error('arn_internal_server_error');
		}
	};
}

In this case, I was hoping that I could get around the issue of having variable numbers of params by hardcoding generics for a reasonable max number of parameters. My hope was that, that for Closures with less than 5 params, the unused PX template types would default to never, and Closure(never): R would be considered identical to Closure(): R.

Of course, it didn't actually work when I tried it, and I suspect that not even setting explicit defaults for the template types (once #1835 lands) would alter the behavior here.

But should it work? Is a parameter of type never the same thing as no parameter at all? Or would that conceptually be more like a void parameter, if such a thing even makes sense?


It's also worth noting that TypeScript has a Parameters utility type. Presumably, if PHPStan had something like this, the kind of static typing I'm looking for would be straightforward, using syntax that could look a little like this:

/**
 * @template R
 *
 * @param (Closure(...): R) $fn
 *
 * @return (Closure(...parameters<$fn>): (R|WP_Error))
 */

What do you think of these ideas?

Originally posted by @ZebulanStanphill in #6813 (comment)

Activity

  1. added this to the Generics milestone on Oct 26, 2022
  2. ondrejmirtes commented on Dec 22, 2022

    @ondrejmirtes
    MemberAuthor

    Another idea for the syntax:

    callable(function-parameters<doFoo>): void
    callable(method-parameters<T, doFoo>): void
    callable(constructor-parameters<T>): void
    

    Also we'd need return-type pseudotype for the same reason.

  3. weirdan commented on Dec 23, 2022

    @weirdan
  4. pionl commented on Dec 23, 2022

    @pionl

    @ondrejmirtes Sorry for the duplicate #8581, did not find the duplicate under 'Closure' term :(

    But, when the closure is desired to be dynamic then what would the syntax look like? I mean when used with methods like Laravel's container->call function that calls the closure with resolved parameters? Then mixed... is then desired, right?

  5. ondrejmirtes commented on Dec 23, 2022

    @ondrejmirtes
    MemberAuthor

    We should always be able to provide a precise signature for the thing that is called. Feel free to show an example on phpstan.org with expected behaviour.

    If you don't care about the signature and want less type checking, you can always type just callable or Closure without additional details.

  6. pionl commented on Dec 23, 2022

    @pionl

    Thank you for the reply. I think that with dependency injection it is little bit different. This is an example from the issue .

    A real code is here:

    The solution as you are proposing is passing Container to the closure and let the consumer manually make classes from the container. This would cause slightly less attractive DX.

    If its not in aim of PHPStan to support mixed... to help improve type static, then I understand your point of view.

    Thanks and happy holidays

  7. phpstan-bot commented on May 17, 2023

    @phpstan-bot
    Contributor

    @pionl After the latest push in 1.11.x, PHPStan now reports different result with your code snippet:

    @@ @@
    -45: Parameter #1 $createState of method ContextService::get() expects Closure(...mixed): Value, Closure(string): Value given.
    +45: Parameter #1 $createState of method ContextService::get() expects Closure(mixed ...): Value, Closure(string): Value given.
    Full report
    Line Error
    45 Parameter #1 $createState of method ContextService::get() expects Closure(mixed ...): Value, Closure(string): Value given.
  8. pionl commented on Jul 18, 2023

    @pionl

    Hi @ondrejmirtes any chance of rethinking this?

    I do not need to check closure parameters, I just need the type of the return value :) The "hack" solution does not work on Level 9 :)

  9. pionl commented on Jul 18, 2023

    @pionl

    I've tried to do an extension / locate the source code where the types are handled but no success :(

  10. phpstan-bot commented on Mar 24, 2026

    @phpstan-bot
    Contributor

    @pionl After the latest push in 2.2.x, PHPStan now reports different result with your code snippet:

    @@ @@
    -45: Parameter #1 $createState of method ContextService::get() expects Closure(...mixed): Value, Closure(string): Value given.
    +38: Callable passed to call_user_func_array() invoked with 1 parameter, 6 required.
    +45: Parameter #1 $createState of method ContextService::get() expects Closure(mixed ...): Value, Closure(string): Value given.
    Full report
    Line Error
    38 Callable passed to call_user_func_array() invoked with 1 parameter, 6 required.
    45 Parameter #1 $createState of method ContextService::get() expects Closure(mixed ...): Value, Closure(string): Value given.
  11. ruudk commented on Jun 18, 2026

    @ruudk
    Contributor

    When working on framework/library related things, that want to expose nice API's to end-users, we often want to accept a callable that has variable types. Using reflection or other logic, we can then call the callable with some payload.

    See this example below:

    #[Attribute(flags: Attribute::TARGET_METHOD)]
    final readonly class Idempotency
    {
        /**
         * @param Closure(): ?IdempotencyKey $key
         */
        public function __construct(
            public Closure $key,
        ) {}
    }
    
    final class SomeHandler
    {
        #[Idempotency(
            key: static function (SomeCommand $command, UserActor $actor) : IdempotencyKey {
                return new IdempotencyKey('some', $command->id, $actor->userId);
            },
        )]
        public function __invoke(SomeCommand $command, UserActor $actor) : void
        {
        }
    }

    The end-user can use the #[Idempotency] attribute and pass a closure. The payload of the closure, is the exact same payload as the __invoke method's signature. It just depends on what the end-user needs to calculate the Idempotency Key.

    It's currently impossible to type this situation in PHPStan. I tried using Closure(mixed...) but that results in:

    Parameter $key of attribute class Idempotency constructor expects Closure(mixed ...): (IdempotencyKey|null), Closure(SomeCommand, UserActor): IdempotencyKey given.

    @ondrejmirtes suggested:

    callable(function-parameters<doFoo>): void
    callable(method-parameters<T, doFoo>): void
    callable(constructor-parameters<T>): void
    

    Which would be epic. I can already tell a few examples where I might be able to use that. But in situations with Attributes, I don't think that will work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions