laravel/framework · error · InvalidArgumentException

Callback must be a callable, callback array, or a 'Class@met

Error message

Callback must be a callable, callback array, or a 'Class@method' string.

What it means

Thrown by Gate::define() when the supplied $callback is neither a PHP callable, an array whose first element is a class string, nor a 'Class@method' string. The Gate rejects any other type so that ability resolution never silently stores an unusable handler.

Source

Thrown at src/Illuminate/Auth/Access/Gate.php:212

     *
     * @throws \InvalidArgumentException
     */
    public function define($ability, $callback)
    {
        $ability = enum_value($ability);

        if (is_array($callback) && isset($callback[0]) && is_string($callback[0])) {
            $callback = $callback[0].'@'.$callback[1];
        }

        if (is_callable($callback)) {
            $this->abilities[$ability] = $callback;
        } elseif (is_string($callback)) {
            $this->stringCallbacks[$ability] = $callback;

            $this->abilities[$ability] = $this->buildAbilityCallback($ability, $callback);
        } else {
            throw new InvalidArgumentException("Callback must be a callable, callback array, or a 'Class@method' string.");
        }

        return $this;
    }

    /**
     * Define abilities for a resource.
     *
     * @param  string  $name
     * @param  string  $class
     * @param  array|null  $abilities
     * @return $this
     */
    public function resource($name, $class, ?array $abilities = null)
    {
        $abilities = $abilities ?: [
            'viewAny' => 'viewAny',
            'view' => 'view',

View on GitHub (pinned to bd6b5437e6)

Solutions

  1. Verify the callback is a Closure/callable, a ['Class','method'] array, or a 'Class@method' string before calling define().
  2. If referencing a class string, confirm the class exists and the method is public.
  3. Use Gate::policy() or a policy class mapping instead of manual define() when authorizing models.
  4. Add a unit test that asserts is_callable($callback) (or Str::parseCallback validity) for dynamic registrations.

Example fix

// before
Gate::define('update-post', $maybeNull);

// after
use Illuminate\Support\Str;

if (is_callable($maybeNull) || Str::parseCallback($maybeNull) !== [null, null]) {
    Gate::define('update-post', $maybeNull);
}
Defensive patterns

Strategy: validation

Validate before calling

use Illuminate\Support\Str;

function isValidGateCallback($callback): bool
{
    return is_callable($callback)
        || (is_string($callback) && Str::parseCallback($callback) !== [null, null])
        || (is_array($callback) && isset($callback[0], $callback[1]) && is_string($callback[0]));
}

if (! isValidGateCallback($cb)) {
    throw new InvalidArgumentException('Refusing to define ability with invalid callback.');
}

Type guard

function isGateCallback(mixed $callback): bool
{
    return is_callable($callback) || is_string($callback);
}

Try / catch

try {
    Gate::define('update-post', $callback);
} catch (\InvalidArgumentException $e) {
    // log which ability/callback failed registration and fall back to a policy
    logger()->error('Invalid Gate callback', ['message' => $e->getMessage()]);
}

Prevention

When it happens

Trigger: Calling Gate::define('update', $someValue) where $someValue is null, an integer, an object that is not callable, or a malformed array like ['Class', 123]; also triggered indirectly through Gate::resource() when the class argument is not a string.

Common situations: Passing a non-existent class@method string that resolves to a non-callable, passing null when a policy class was expected, or a typo where a variable holding a closure is accidentally overwritten before define().

Related errors


AI-assisted analysis of laravel/framework@bd6b5437e6 (2026-08-06). Data as JSON: /data/errors/443e157e9176ad8b.json. Report an issue: GitHub.