多租户是指一种软件架构,在这种架构里,一个应用实例可以同时服务多个租户。
每个租户的数据都是隔离的,对其他租户不可见。这种做法在软件即服务(SaaS)应用中很常见。在本教程中,我们将使用 Laravel 和 Neon 构建一个多租户的 SaaS 应用程序的基础。
学完本教程后,您将拥有一个功能齐全的多租户的 SaaS 应用,租户可以自己管理书籍、用户和设置,同时确保租户之间数据的隔离。
前提条件在我们开始之前,请确保你已经准备好以下这些。
- 已在您的系统中安装了 PHP 8.1 或更高版本
- Composer 用于管理 PHP 依赖
- Node.js 和 npm 用于管理前端资源
- 一个用于托管数据库的 Neon 账户
- 具备 Laravel 和 Livewire 的基础知识
我们先创建一个新的 Laravel 项目并设置所需组件。
创建一个新的 Laravel 项目
打开你的终端并输入以下命令来创建一个新的 Laravel 项目:
composer create-project laravel/laravel laravel-multi-tenant-saas
使用 Composer 安装 Laravel 项目,并进入项目目录。
cd laravel-multi-tenant-saas
全屏模式 退出全屏
安装必要的软件包
对于我们多租户的SaaS应用程序,我们将使用以下套餐。
stancl/tenancy
:一个灵活的 Laravel 多租户包插件- Laravel Breeze:一个简约的 Laravel 认证启动模板
先安装 stancl/tenancy
包:
执行此命令以安装 `stancl/tenancy` 包。
composer require stancl/tenancy
全屏模式:点击进入/退出
安装完包之后,让我们设置租户:
php artisan tenancy:install
运行此命令来安装多租户支持
进入全屏模式。退出全屏模式。
在 bootstrap/providers.php
文件中,添加 TenancyServiceProvider
的注册。
return [
// 返回包含租户服务提供者的数组
// Returns an array containing the Tenancy service provider
App\Providers\TenancyServiceProvider::class,
],
全屏 退出全屏
我们来安装包含 Blade 模板的 Laravel Breeze 这个框架。
composer require laravel/breeze --dev # 安装Laravel Breeze开发依赖
php artisan breeze:install blade # 安装Blade模板安装程序
全屏显示;退出全屏
接下来,请安装所需的 NPM 包(模块)。
npm install
运行 `npm run dev`
以下是这两个命令的简要说明:
npm install
:安装项目所需的依赖包。运行 npm run dev
:启动开发模式。
全屏 退出全屏
搭建数据库
更新你的 .env
文件,添加 Neon 数据库的凭证(例如用户名和密码)。
DB_CONNECTION=pgsql
DB_HOST= 你的neon主机名.neon.tech
DB_PORT= 5432
DB_DATABASE= 你的数据库名称
DB_USERNAME= 你的用户名
DB_PASSWORD= 你的密码
进入全屏模式,退出全屏模式
你可以在此更新了 .env
文件之后,运行数据库迁移命令:
php artisan migrate
这是一个用于Laravel框架的命令。在Laravel中,你经常会用到这个命令:php artisan migrate
。
开启全屏模式,退出全屏
实现多租户模式我们现在有了基本设置,接下来让我们在应用程序里实现多租户吧。
构建租户模型
创建一个 Tenant
模型(租户):
注意在 模型
后添加了冒号,以匹配源文本的标点,并在 Tenant
后添加了注释以解释其含义。
php artisan make:model Tenant // 生成一个名为Tenant的模型
全屏显示 退出全屏
修改 app/Models/Tenant.php
文件:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasFactory, HasDatabase, HasDomains;
}
全屏显示 退出全屏
此模型扩展了由tenancy包提供的基础Tenant
模型,并实现了TenantWithDatabase
接口,定义了租户的可填充属性和自定义列。
HasDatabase
和 HasDomains
这两个特性 HasDatabase
和 HasDomains
由租户包提供的,允许我们管理每个租户的特定数据库和域名。这样每个租户将拥有自己的数据库和域名,确保数据隔离。
要了解更多关于租户套餐事件系统以及如何自定义租户模型,请参阅stancl/tenancy 文档。
配置租户
请修改 config/tenancy.php
文件来使用我们自定义的 Tenant
模型。
'tenant_model' => \App\Models\Tenant::class,
进入全屏;退出全屏
另外,更新域名设置,增加句子的流畅性。
// 中央域名配置
'central_domains' => [
'laravel-multi-tenant-saas.test',
'localhost',
'example.com',
],
切换到全屏 撤回全屏
用您自己的域名替换默认的中央域名。
这是非常重要的一部分,因为这是如何根据租户套餐计划来决定哪个域名(domain)属于哪个租户并加载相应的特定于租户的数据。
随意浏览《config/tenancy.php》文件中的其他配置选项,根据您的需要调整租户设置,以满足您的需求。
租户迁移
租户包内置了事件监听器,当创建租户时会自动执行租户相关的迁移。我们需要确保所有租户相关的迁移都位于 database/migrations/tenant
目录中。
每个租户都将有自己的数据库,所以租户目录下的迁移文件将用于在各自的数据库中创建租户特定的表。
首先复制默认的用户迁移文件到 database/migrations/tenant
目录。
命令行指令:
cp "database/migrations/0001_01_01_000000_create_users_table.php" "database/migrations/tenant"
进入全屏,退出全屏
这将是用于特定租户的表的基础迁移步骤。
实现租户路由功能
租户套餐提供了中间件支持来处理特定于租户的路由需求。这样可以让你定义仅对租户可访问,而非中央域的路由。
首先,创建一个新的文件 routes/tenant.php
,用于特定租户的路由内容如下:
<?php
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
/*
|--------------------------------------------------------------------------
| 租户路由表
|--------------------------------------------------------------------------
|
| 在这里,您可以为您的应用程序注册租户路由。这些路由由TenantRouteServiceProvider提供。
|
| 您可以根据需要自定义这些路由。祝您开发顺利!
|
*/
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return '这是您的多租户应用,当前租户ID为 ' . tenant('id');
});
// 您可以在下面添加更多特定于租户的路由
});
进入全屏模式,退出全屏模式
这些路由将通过 TenantRouteServiceProvider
加载,并且仅对租户可用,。InitializeTenancyByDomain
中间件将根据域名来设置当前租户,而 PreventAccessFromCentralDomains
中间件将防止从中心域名访问,以确保安全。
了解更多如何自定义租户路由的信息,请参阅以下链接:stancl/tenancy 文档。
租户创建流程
创建一个租户账号注册的控制器,这一般是由应用程序的管理员用户来完成的。
在你的终端中输入以下代码来运行命令并创建一个控制器:
php artisan make:controller TenantController
注:TenantController
可以根据需要翻译为“租户控制器”,但在技术社区中它通常保持英文不变。
点击进入全屏模式,点击退出全屏。
更新 app/Http/Controllers/TenantController.php
控制器,实现租户的注册功能。
<?php
namespace App\Http\Controllers;
use App\Models\Tenant;
use Illuminate\Http\Request;
class TenantController extends Controller
{
public function showRegistrationForm()
{
return view('tenant.register');
}
public function register(Request $request)
{
$request->validate([
'domain' => 'required|string|max:255|unique:domains,domain',
]);
$tenant = Tenant::create();
$tenant->domains()->create(['domain' => $request->domain]);
return redirect()->route('tenant.registered', $request->domain);
}
public function registered($domain)
{
return view('tenant.registered', compact('domain'));
}
}
切换到全屏 退出全屏
此控制器处理租户注册,在数据库中创建一个新的租户,并设置租户的域名。TenancyServiceProvider
会自动将租户事件映射给监听器,监听器将为新租户创建数据库,并在该目录 database/migrations/tenant
中运行特定于该租户的迁移。
简单来说,控制器包含三种不同的方法:
showRegistrationForm()
: 显示租户注册表register()
: 注册新租户,创建租户记录和域名registered()
: 显示成功注册消息
此控制器将用于管理我们应用程序中的租户注册流程。它允许新租户注册并为他们自己的账户创建子域名和数据库。
在 routes/web.php
中添加租户注册的路由配置:
use App\Http\Controllers\TenantController;
Route::get('/register', [TenantController::class, 'showRegistrationForm'])->name('tenant.register');
Route::post('/register', [TenantController::class, 'register']);
Route::get('/registered/{domain}', [TenantController::class, 'registered'])->name('tenant.registered');
切换到全屏模式,退出全屏
创建租户注册视图,从开始创建 resources/views/tenant/register.blade.php
文件:
<x-guest-layout>
<form method="POST" action="{{ route('tenant.register') }}">
@csrf
<div class="mt-4">
<x-input-label for="domain" :value="__('域名')" />
<div class="flex">
<x-text-input
id="domain"
class="mt-1 block w-full"
type="text"
name="domain"
:value="old('domain')"
required
/>
<span class="text-gray-600 ml-2 mt-1">.example.com</span>
</div>
</div>
<div class="mt-4 flex items-center justify-end">
<x-primary-button class="ml-4"> {{ __('注册用户') }} </x-primary-button>
</div>
</form>
</x-guest-layout>
全屏模式 全屏退出
然后创建一个 resources/views/tenant/registered.blade.php
文件,以便在用户注册完成后显示成功的注册消息。
<x-guest-layout>
<div class="text-gray-600 mb-4 text-sm">
{{ __('您的租户已成功注册啦!') }}
</div>
<div class="mt-4 flex items-center justify-between">
<div>
您的租户链接:
<a
href="https://{{ $domain }}.example.com"
class="text-gray-600 hover:text-gray-900 text-sm underline"
target="_blank"
>https://{{ $domain }}.example.com (请将{{ $domain }}替换为您的实际域名)</a
>
</div>
</div>
</x-guest-layout>
进入全屏模式,退出全屏模式
这标志着租户注册流程的完成。现在,租户可以注册并为自己的账户创建子域和数据库。在实际情况中,您会用认证中间件来保护注册路由,从而确保只有授权的管理员用户才能创建新的租户。
租户身份验证
为了验证注册流程是否有效,请访问 http://laravel-multi-tenant-saas.test/register
并注册一个新的客户或租户。完成注册后,你应该会看到成功消息,消息中包含租户的域名。
接下来,请进入您的Neon仪表盘,确认新租户的数据库是否建立:
SELECT * FROM tenants;
从租户表中选择所有数据。
进入全屏 退出全屏
你应该在 tenants
表中看到新创建的租户信息。你也可以检查 domains
表,确认租户的域名是否已经添加成功。
SELECT * FROM domains;
全屏模式,退出全屏
首先,通过 psql
控制台中的 \l
命令来列出所有数据库,或者使用以下 SQL 查询来确认确实为新的租户有一个独立的数据库:
SELECT datname FROM pg_database WHERE datistemplate = false;
下面的SQL语句会列出所有非模板数据库的名字。
进入全屏,退出全屏
租户的数据库应该出现在结果中,并且名称应为tenant{租户ID}
。
租户配置包允许您为租户配置数据库命名约定。默认情况下,数据库名称为 tenant{tenant_id}
,其中 {tenant_id}
是租户 ID。您还可以配置套餐,使其为租户使用单独的模式而不是单独的数据库。
搞定这些之后,你就成功地在你的多租户SaaS应用程序中实现了租户注册流程。接下来实现租户的入驻过程。
租户入驻实施
现在你可以注册新用户了,我们来创建一个上手流程吧。
每个租户都需要创建一个账户来访问他们的控制面板。域名将用来标识租户,因此我们将使用租户的域名作为子域,例如 tenant1.example.com
。
创建一个新的控制器来帮助租户入驻:
运行以下命令创建一个控制器:php artisan make:controller Tenant/OnboardingController
。
全屏 退出全屏
修改 app/Http/Controllers/Tenant/OnboardingController.php
文件,处理入驻流程。
<?php
namespace App\Http\Controllers\Tenant;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class OnboardingController extends Controller
{
public function show()
{
if (User::count() > 0) {
return redirect()->route('tenant.dashboard');
}
return view('tenant.onboarding');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
auth()->login($user);
return redirect()->route('tenant.dashboard')->with('success', '欢迎使用您的新账户!');
}
}
全屏查看 退出全屏
在 routes/tenant.php
文件中的 Route::middleware
组里为入驻流程的租户路由添加路由。
use App\Http\Controllers\Tenant\OnboardingController;
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
// Existing routes
// ...
Route::get('/onboarding', [OnboardingController::class, 'show'])->name('tenant.onboarding');
Route::post('/onboarding', [OnboardingController::class, 'store'])->name('tenant.onboarding.store');
});
点击进入全屏模式 切换到退出全屏模式
在 resources/views/tenant/onboarding.blade.php
中创建引导页面:
<x-guest-layout>
<form method="POST" action="{{ route('tenant.onboarding.store') }}">
@csrf
<div>
<x-input-label for="name" :value="__('姓名')" />
<x-text-input
id="name"
class="mt-1 block w-full"
type="text"
name="name"
:value="old('name')"
required
autofocus
autocomplete="name"
/>
</div>
<div class="mt-4">
<x-input-label for="email" :value="__('电子邮件')" />
<x-text-input
id="email"
class="mt-1 block w-full"
type="email"
name="email"
:value="old('email')"
required
autocomplete="username"
/>
</div>
<div class="mt-4">
<x-input-label for="password" :value="__('密码')" />
<x-text-input
id="password"
class="mt-1 block w-full"
type="password"
name="password"
required
autocomplete="new-password"
/>
</div>
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('确认您的密码')" />
<x-text-input
id="password_confirmation"
class="mt-1 block w-full"
type="password"
name="password_confirmation"
required
autocomplete="new-password"
/>
</div>
<div class="mt-4 flex items-center justify-end">
<x-primary-button class="ml-4"> {{ __('设置完成') }} </x-primary-button>
</div>
</form>
</x-guest-layout>
点击进入全屏;点击退出全屏
为了简化,我们将扩展 Breeze 访客布局来扩展引导表单。但你可以自定义布局以匹配应用程序的设计,甚至可以为每个租户定制不同的布局以满足其具体需求。
测试入职流程测试,请访问http://tenant1.example.com/onboarding
,然后完成入职表单。提交表单后,您应该会被跳转到这个租户仪表盘,我们将在后续实现该功能。
租户仪表盘的实施
为租户界面创建一个新的管理面板:
php artisan make:controller Tenant/DashboardController
切换到全屏 退出全屏
修改代码 app/Http/Controllers/Tenant/DashboardController.php
以显示租户的仪表盘:
<?php
namespace App\Http\Controllers\Tenant;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
/**
* 租户仪表板控制器
*/
class DashboardController extends Controller
{
/**
* 显示租户仪表板
*/
public function index()
{
return view('tenant.dashboard');
}
}
全屏
退出全屏
在 resources/views/tenant/dashboard.blade.php
文件中创建仪表板视图:
<x-app-layout>
<x-slot name="header">
<h2 class="text-gray-800 text-xl font-semibold leading-tight">{{__('首页')}}</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl lg:px-8 sm:px-6">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="text-gray-900 p-6">{{__("您已成功登录!")}}</div>
</div>
</div>
</div>
</x-app-layout>
进入全屏模式,退出全屏
在 routes/tenant.php
文件中的 Route::middleware
组中为租户仪表板添加一条路由。
使用如下 App\Http\Controllers\Tenant\DashboardController;
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
// 现有路由
// ...
Route::get('/dashboard', [DashboardController::class, 'index'])->name('tenant.dashboard');
});
// 此代码用于...
进入全屏 退出全屏
要在完成开通流程后,请访问 http://tenant1.example.com/dashboard
,你应该会看到带有欢迎消息的仪表盘。
您也可以检查租户的数据库中的 users
表,以验证在入驻过程中创建的用户账号是否已加入:
SELECT * FROM users;
从用户表中选择所有数据。
切换到全屏 退出全屏
这会显示出该特定租户数据库中在注册过程中创建的用户账户,而不是中央数据库里的账户。
结尾在本教程中,我们已经构建了一个简单的多租户应用程序,使用 Laravel 和 Neon。我们涵盖了以下内容:
- 设置项目并实现多租户功能
- 建立租户注册流程
- 实现租户入驻流程
- 为每个租户添加一个专属仪表板
这种实现为使用 Laravel 和 Neon 来构建更复杂的 SaaS 应用提供了基础。您可以进一步扩展或增强该系统,
- 为租户仪表盘添加更多功能
- 实施计费和订阅管理
- 通过双因素验证来增强安全性
- 添加针对租户的自定义功能
使用 stancl/tenancy
包及 Neon,每个租户都将有自己的独立数据库。由于 Neon 的自动扩展功能,您可以轻松地随着租户数量的增加来扩展您的应用程序。
还有其他包和工具可以帮助你使用 Laravel 构建多租户应用程序。你可以根据需求探索这些选项,并选择最适合的一个合适的。一些流行的包包括:
- spatie/laravel-multitenancy(介绍和文档)
- tenancy/tenancy(GitHub项目页面)
共同学习,写下你的评论
评论加载中...
作者其他优质文章