laravel/framework · error · TypeError

Illuminate\Container\Container::bind(): Argument #2 ($concre

Error message

Illuminate\Container\Container::bind(): Argument #2 ($concrete) must be of type Closure|string|null

What it means

A PHP TypeError raised by Container::bind() when the $concrete argument is not a Closure, a string, or null. The container wraps string concretes in a closure and treats null as the abstract itself, so any other type (array, object instance, int) is rejected at the type boundary.

Source

Thrown at src/Illuminate/Container/Container.php:381

                $abstract, $concrete, $shared
            );
        }

        $this->dropStaleInstances($abstract);

        // If no concrete type was given, we will simply set the concrete type to the
        // abstract type. After that, the concrete type to be registered as shared
        // without being forced to state their classes in both of the parameters.
        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            if (! is_string($concrete)) {
                throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
            }

            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    /**
     * Get the Closure to be used when building a type.
     *

View on GitHub (pinned to e0f6eb3518)

Solutions

  1. Wrap the callable in a Closure: bind('key', fn ($app) => $app->make(SomeClass::class)).
  2. If you want a single instance, use $container->singleton('key', $concrete) or $container->instance('key', $instance) for a pre-built object.
  3. Pass a class-name string so the container auto-wraps it: bind('abstract', Concrete::class).
  4. If passing null is intended (bind concrete to itself), pass null explicitly rather than omitting.

Example fix

// before
$app->bind('cache.repo', [RedisRepo::class, 'make']);
// after
$app->bind('cache.repo', fn ($app) => $app->make(RedisRepo::class));
// or for a pre-built object
$app->instance('cache.repo', $repo);
Defensive patterns

Strategy: type-guard

Validate before calling

function assertBindableConcrete(mixed $concrete): void
{
    if ($concrete !== null && ! $concrete instanceof \Closure && ! is_string($concrete)) {
        throw new TypeError('bind(): concrete must be Closure|string|null, got ' . get_debug_type($concrete));
    }
}

assertBindableConcrete($concrete);
$container->bind($abstract, $concrete);

Type guard

/** @param mixed $c */
function isBindableConcrete($c): bool
{
    return $c === null || $c instanceof \Closure || is_string($c);
}

Try / catch

try {
    $container->bind($abstract, $concrete);
} catch (\TypeError $e) {
    // convert to a closure / use instance() / singleton() as appropriate
    throw $e;
}

Prevention

When it happens

Trigger: Calling $container->bind('key', ['Class', 'method']); passing an object instance instead of a closure: $container->bind('key', $instance); passing an integer or boolean as concrete.

Common situations: Mistaking bind() for instance() / singleton(); passing a callable array where only Closure|string is accepted; copy-paste from a context that used a different DI API.

Related errors


AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11). Data as JSON: /api/errors/c7da5e566389c783. Report an issue: GitHub.