cakephp/cakephp · error · InvalidArgumentException

Need a valid Closure to connect routes.

Error message

Need a valid Closure to connect routes.

What it means

RouteBuilder::scope() requires a Closure to define nested routes. If neither the $params argument nor the third argument is a Closure (i.e. $callback stays null), the builder throws this InvalidArgumentException, because a scope without a callback has no routes to connect.

Solutions

  1. Pass a closure as the callback: $routes->scope('/api', function ($routes) { ... });
  2. If you intended options, pass them in the array argument and still provide the Closure last: scope('/api', ['param' => 1], function ($routes) {...}).
  3. Check for null callback variables before calling scope().

Example fix

// before
$routes->scope('/api', 'routes');
// after
$routes->scope('/api', function (RouteBuilder $routes) {
    $routes->connect('/ping', ['controller' => 'Ping', 'action' => 'index']);
});
Defensive patterns

Strategy: type-guard

Validate before calling

if (!$callback instanceof \Closure) {
    throw new InvalidArgumentException('scope() requires a Closure callback');
}

Type guard

function isValidScopeCallback(mixed $cb): bool { return $cb instanceof \Closure; }

Prevention

When it happens

Trigger: $routes->scope('/api') with no closure; passing something other than a Closure (e.g. an array or a string) as the callback; transposing the $params and $callback arguments.

Common situations: Refactor stubs leaving scope() without a body; copy-paste from non-closure route APIs; dynamic code building scopes where the callback variable is conditionally null.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of cakephp/cakephp@1128eba9b0 (2026-09-12). Data as JSON: /api/errors/36bb9d38bcfb8afd. Report an issue: GitHub.

Appendix: source

Thrown at src/Routing/RouteBuilder.php:959

     *
     * - `_namePrefix` Set a prefix used for named routes. The prefix is prepended to the
     *   name of any route created in a scope callback.
     *
     * @param string $path The path to create a scope for.
     * @param \Closure|array $params Either the parameters to add to routes, or a callback.
     * @param \Closure|null $callback The callback to invoke that builds the plugin routes.
     *   Only required when $params is defined.
     * @return $this
     * @throws \InvalidArgumentException when there is no callable parameter.
     */
    public function scope(string $path, Closure|array $params, ?Closure $callback = null)
    {
        if ($params instanceof Closure) {
            $callback = $params;
            $params = [];
        }
        if ($callback === null) {
            throw new InvalidArgumentException('Need a valid Closure to connect routes.');
        }

        if ($this->_path !== '/') {
            $path = $this->_path . $path;
        }
        $namePrefix = $this->_namePrefix;
        if (isset($params['_namePrefix'])) {
            $namePrefix .= $params['_namePrefix'];
        }
        unset($params['_namePrefix']);

        $params += $this->_params;
        $builder = new static($this->_collection, $path, $params, [
            'routeClass' => $this->_routeClass,
            'extensions' => $this->_extensions,
            'namePrefix' => $namePrefix,
            'middleware' => $this->middleware,
        ]);

View on GitHub (pinned to 1128eba9b0)