topthink/think-multi-app
topthink/think-multi-app adds multi-application support to ThinkPHP, letting one codebase serve separate apps with independent routing, controllers, and configuration. It enables app-aware URL dispatch and domain binding for cleaner multi-site projects.
Install it once and treat it as infrastructure — there is nothing to configure for the default case:
composer require topthink/think-multi-app
The service provider is auto-discovered from composer.json (extra.think.services), so no manual registration is needed. It injects a global MultiApp middleware that runs before route matching and rewrites the current app based on the request.
Immediately after installing, the biggest trap is that the package assumes the default single-app files have already been moved. Do one of the following before the first request:
app/controller, app/model, app/view into app/index/ (and keep app/index/ as the default app), orphp think build index (and php think build admin for a second app).Check config/app.php and confirm:
'auto_multi_app' => true,
'default_app' => 'index',
'app_map' => [],
First use case: a two-app split — public site + admin panel:
app/
├── index/
│ ├── controller/Index.php → app\index\controller\Index
│ ├── view/
│ └── route/app.php
└── admin/
├── controller/Dashboard.php → app\admin\controller\Dashboard
└── route/app.php
A request to http://site.test/admin/dashboard/index resolves to app/admin/controller/Dashboard::index(). A request to http://site.test/ falls back to default_app (index).
Where to look first:
vendor/topthink/think-multi-app/src/MultiApp.php — the parseMultiApp() method is the whole package; read it once and the behavior is transparent.config/app.php — auto_multi_app, default_app, app_map, app_express.route/ and app/<app>/route/ — to understand where routing is loaded from in multi-app mode.App per bounded context, not per page. The idiomatic layout is one app per delivery surface: index (public), admin (back office), api (JSON), mobile (H5). Each gets its own controllers, views, route files, and middleware, which keeps cross-cutting concerns isolated.
Domain/subdomain binding is the cleanest production pattern. Instead of leaking the app name into every URL, map hosts to apps:
// config/app.php
'app_map' => [
'www.example.com' => 'index',
'admin.example.com' => 'admin',
'api.example.com' => 'api',
],
Now https://admin.example.com/dashboard/index hits the admin app without an admin/ URL segment. This is the pattern most day-to-day work should use.
URL generation stays app-aware. Link helpers accept the app name as the first segment:
url('admin/user/edit', ['id' => $id]); // /admin/user/edit?id=...
url('index/index/index'); // /index/index/index
In practice, either hard-code the app prefix or wrap this in a small helper so templates don't scatter app names.
Per-app routes. Route definitions live inside each app, not at the project root:
// app/admin/route/app.php
Route::group('admin', function () {
Route::get('dashboard', 'Dashboard/index');
});
Because MultiApp runs before the route middleware, the route loader picks up the already-resolved app and only loads that app's route files. Keep app-specific route files self-contained.
Per-app middleware and hooks. Each app directory can carry its own middleware.php, common.php, and event.php, so auth, CORS, and rate limiting can be scoped to admin or api without polluting the public site. Register shared middleware globally and scope the rest per app.
Shared code goes outside the app folders. Don't reach from admin into index by relative path. Put reusable services, traits, and value objects in:
app/common/ app, orextend/, orDependency direction should be: apps → shared, never app → app.
Namespacing is automatic. app/admin/controller/User.php must declare namespace app\admin\controller;. Composer's PSR-4 mapping for app\ → app/ handles the rest; no per-app autoload entries needed.
Short URLs with app_express. When enabled, a single-segment URL like /admin can be interpreted as an app + default controller/action, letting you expose /admin instead of /admin/index/index. Useful for pretty landing URLs per app; disable it if you want the full app/controller/action shape enforced.
Scaffolding a new app is a one-liner when the command is available (php think list will confirm it): php think build api creates app/api/ with the standard skeleton.
app/controller after install. If you skip the restructure, requests 404 because the framework looks for app/<first-segment>/controller/... and finds nothing. This is the #1 install problem./admin/settings) will be hijacked by the app resolver. When in doubt, use app_map with domains so the app name never appears in the path.app_map matches the host exactly. admin.example.com and www.admin.example.com are different keys; ports and www. are not stripped automatically. Normalize the Host header or list every variant.default_app only applies when no app segment is present. A mistyped app name is not silently routed to the default — it produces a 404. That's usually correct behavior, but it confuses people debugging a typo.Index → Index.php, app admin → app/admin/. A Admin/ folder with namespace App\Admin will fail in production after passing on macOS.app/<app>/route/. If your app-specific app.php isn't being read, verify the path, not the route syntax.MultiApp runs before the route middleware, so app selection happens first. If you need a URL handled before app dispatch (health checks, redirects), put it in web-server config or a very early middleware, not a route.app_debug => true) and log the resolved app name inside a temporary middleware placed after MultiApp. When the resolution looks wrong, it's almost always app_map, auto_multi_app, or a missing default_app.'auto_multi_app' => false and resolve the app yourself (e.g. from an X-App header or API version prefix). The middleware then does nothing, and you get full control — this is the documented extension point for header-based or version-based dispatch.public/. Multi-app makes app/<app>/ directories tempting to URL-map directly; doing so exposes source, config, and .env files.php think clear (and any route/schema cache commands you use). Stale compiled config is a common "I moved it and it still 404s" cause.MultiApp.php source for edge cases rather than search for answers. It is maintained by top-think alongside the framework itself, so it's reliable — just lean on framework docs, and treat the middleware file as the reference implementation.How can I help you explore Laravel packages today?