Skip to content

Route cloning

Route cloning is a feature for creating “tenant versions” of central routes. Typically that means:

In other words, route cloning is typically used with path identification. This is not a requirement though as the feature is highly configurable — see the examples below.

Assuming you’ve read the universal routes page, let’s try to reimplement universal routes, but in a way that supports path identification.

Imagine that you’re integrating with a third-party package that has standard routes without any {tenant} parameters:

Route::group([
'middleware' => config('posts_package.middleware'),
], function () {
Route::get('/posts', [PostController::class, 'index'])->name('package.posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');
});

In this example, we can change the middleware the package applies on its routes.

Let’s start by adding the path identification middleware and 'universal' since we want this route to work in both central and tenant contexts:

[
InitializeTenancyByPath::class,
'universal',
]

Now we can run the cloning action:

TenancyServiceProvider.php
protected function mapRoutes()
{
$this->app->booted(function () {
if (file_exists(base_path('routes/tenant.php'))) {
RouteFacade::namespace(static::$controllerNamespace)
->middleware('tenant')
->group(base_path('routes/tenant.php'));
}
$this->cloneRoutes();
});
}
protected function cloneRoutes()
{
/** @var CloneRoutesAsTenant $cloneRoutes */
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
$cloneRoutes->cloneRoutesWithMiddleware(['universal'])->handle();
}

Notice the highlighted segment. We’ve told the package to clone routes that have the universal middleware.

This action will create a copy of all routes have the universal middleware flag, and “aren’t tenant”, meaning they don’t have a {tenant} parameter and their name doesn’t start with tenant.. It will apply the following changes:

  • Prefix the route path with /{tenant}/
  • Prefix the route name with tenant.
  • Apply the tenant flag (to make tenancy initialization work if we’d be using early identification) and remove existing flags, like universal

The example routes above would be cloned like this:

// Original routes — central
Route::get('/posts', [PostController::class, 'index'])->name('package.posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');
// Cloned routes — tenant
Route::get('/{tenant}/posts', [PostController::class, 'index'])->name('tenant.package.posts.index');
Route::get('/{tenant}/posts/{post}', [PostController::class, 'show'])->name('tenant.package.posts.show');

The original routes will be accessible in the central app (even if they have the identification middleware) because they have the universal flag. They will not be accessible in the tenant app since there’s no {tenant} parameter that could be specified.

The cloned routes will only be accessible in the tenant app (since they have the {tenant} parameter) and tenancy will be initialized on them.

The route names are prefixed with tenant. to avoid collisions while still letting you use named routes.

In these setups, you’d also often use the UrlGenerator bootstrapper which can automatically convert route('package.posts.index') calls to route('tenant.package.posts.index', ['tenant' => tenant()->getTenantKey()]) calls when you’re in the tenant context.

Following the previous example, let’s refine it a bit: instead of making the central route universal — we’ve done that just to have a way to pass the tenant identification middleware to the cloned route — we can pass the tenant middleware manually.

We will set this middleware on the group:

[
InitializeTenancyByPath::class,
'universal',
'clone',
]

And clone routes like this:

TenancyServiceProvider.php
protected function cloneRoutes()
{
// ...
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
$cloneRoutes->cloneRoutesWithMiddleware(['universal'])->handle();
$cloneRoutes->addTenantMiddleware([InitializeTenancyByPath::class])->handle();
}

cloneRoutesWithMiddleware() defaults to ['clone'] which is an empty middleware group, just like the central, tenant, and universal flags, except it has no meaning outside of the cloning context — it’s just used to mark a certain route/route group as clonable by the action.

With this change, the original route will just have an unused clone middleware group instead of being unnecessarily marked as universal, and the cloned tenant route will still have the desired tenant identification middleware.

The action is extensively documented by docblocks, so all configuration options will not be covered here, but here are some examples of how the action can be used:

  • shouldClone(fn (Route): bool) to provide a custom callback for determining which routes should be cloned
  • cloneRoutesWithMiddleware([...]) demonstrated above, defaults to ['clone']. The default “should clone” logic looks at this
  • cloneUsing(fn (Route|string): void) to replace the default cloning logic with your own implementation
  • addTenantParameter() whether the cloned route should be prefixed with {tenant}
  • domain() domain scope to be applied on the cloned route, relevant with multi-domain tenancy
  • cloneRoute() / cloneRoutes() to clone individual routes

You could use route cloning with domain identification in a scenario where you’re using a third-party library for authentication and you want to register the same routes in both contexts.

If the routes were simply required, as in:

require __DIR__.'/auth.php';

You could simply place that statement in both routes/web.php and routes/tenant.php. If they are registered by the package itself though, cloning can be helpful.

You could also use universal routes, and in many apps, that does work very well for auth routes.

The main reason for wanting identical-yet-separate auth routes in the two contexts is that you may want to be able to reference e.g. route('auth.login') as the central route from any context — such that the route() call references a domain-bound route.

For custom setups like this, since they vary a lot, you’re encouraged to take a look at the CloneRoutesAsTenant class for usage information. Make sure to read the paragraph about domain() scopes in the class docblock.