今天我们发布了一个名为 laravel-multitenancy 的包,用于使Laravel应用具备多租户功能。这个包的理念是仅提供实现多租户功能的最基本功能。
该包可以确定请求的当前租户应该是什么。它还允许你定义当将当前租户更改为另一个租户时应发生什么。
该包适用于需要使用单一数据库或多个数据库的 一个 或 多个数据库 的多租户项目。
在这篇博客文章中,我想向大家介绍这个包。
您是视觉学习者吗?
Laravel Package Training 包含一个20分钟的教程视频,它将引导您通过一个多数据库演示应用,该应用使用 laravel-multitenancy。展示该包的功能后,我将解释其内部工作原理。
我非常相信,观看这个视频每个人都能学到一些新东西。
为什么创建另一个多租户包?#
Laravel中的多租户制似乎一直是热门话题。我认为这是因为实现多租户制的途径有很多。为了了解多租户制能够包含的内容以及可能的解决方案,我强烈推荐观看Tom Schlick在2017年Laracon US上所做的这场演讲。
因为我在客户项目中从未需要过,所以我一直避开这个话题。
有人可能会说,我构建的Oh Dear(uptime tracker),就是一个支持多租户制的项目。我们在那里使用了非常轻量级的解决方案。我们只是在sites
表中添加了一个team_id
。当有人登录时,我们会检查用户所属的团队,并只为这些站点显示信息。我认为对于大多数项目来说,这样的单一数据库解决方案效果很好。
最近,我开始着手一个新的客户项目,这个项目应该是多租户制的。这个项目的需求表明,每个租户应该有自己的数据库。在我研究这个话题的时候,穆罕默德·萨伊德(Mohammed Said)发布了一系列关于多租户制的视频,这真是一种纯粹的巧合。
在这些视频中,穆罕默德分享了一个非常轻量级的解决方案。这似乎表明多租户制并没有我想象的那么难。我决定打包他的解决方案。在这样做的同时,通过与穆罕默德的讨论,我对一个多租户包应该做什么有了更深入的了解。
大多数现有的包对我来说感觉太重了。我想知道为什么是这样。我认为任何多租户包都应该做这三件事:
- 应该跟踪当前租户是谁
- 在将一个租户设置为当前租户时应动态更改Laravel应用程序的配置(更改数据库、前缀缓存)
- 例如,为租户创建新数据库的工具,或者迁移租户的工具
对我来说,现有的包做的事情太多了。它们中的大多数在1.方面做得很好,但也过于关注“2.”和“3.”。
我认为在需要租户意识的项目中,每个项目都需要做一些特定的事情来将租户设置为当前租户。我们不尝试处理所有不同的案例,而是决定使定义使租户成为当前租户的任务变得简单。这样,实现上面的“2。”保持非常轻量。
对于“3.”工具,我的同事Seb有一个很好的想法。我们并不是创建特定的租户命令,而是让现存的命令容易地感知租户。这就是我们做到的。在本文的后续部分,我会解释这个解决方案。
通过将“2.”和“3.”olutions保持非常通用,包保持了轻量。
跟踪当前租户
您安装包后,应用程序将有一个包含每个应用程序租户的行的tenants
表。
为了确定给定请求中应使用哪个租户作为当前租户,该软件包使用TenantFinder
。任何扩展Spatie\Multitenancy\TenantFinder\TenantFinder
的类都是有效的租户查找器。这个抽象类的样子是这样的
abstract public function findForRequest(Request $request): ?Tenant;
该软件包附带一个名为DomainTenantFinder
的类。该类将尝试找到具有匹配当前请求主机名的domain
属性的Tenant
。
以下是默认的DomainTenantFinder
的实现。该getTenantModel
方法返回在multitenancy
配置文件中的tenant_model
键指定的类的实例。
namespace Spatie\Multitenancy\TenantFinder;
use Illuminate\Http\Request;
use Spatie\Multitenancy\Models\Concerns\UsesTenantModel;
use Spatie\Multitenancy\Models\Tenant;
class DomainTenantFinder extends TenantFinder
{
use UsesTenantModel;
public function findForRequest(Request $request):?Tenant
{
$host = $request->getHost();
return $this->getTenantModel()::whereDomain($host)->first();
}
}
在 multitenancy
配置文件中,您可以使用 tenant_finder
键指定租户查找器。
// in multitenancy.php
/*
* This class is responsible for determining which tenant should be current
* for the given request.
*
* This class should extend `Spatie\Multitenancy\TenantFinder\TenantFinder`
*
*/
'tenant_finder' => Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
这使得自定义如何确定当前租户变得更加容易。
有几种方法可以获取、设置和清除当前租户。
您可以通过这种方式找到当前方法。
Spatie\Multitenancy\Models\Tenant::current(); // returns the current tenant, or if not tenant is current, `null`
当前租户还会使用 currentTenant
键绑定在容器中。
app('currentTenant'); // returns the current tenant, or if not tenant is current, `null`
您可以检查是否已设置租户作为当前租户
Tenant::checkCurrent() // returns `true` or `false`
您可以通过调用其上的 makeCurrent()
方法来手动将租户设置为当前租户。
$tenant->makeCurrent();
定义应在将租户设置为当前租户时运行的作业
当租户被设置为当前租户时,该软件包将运行 multitenancy
配置文件中 switch_tenant_tasks
键配置的所有任务。
此软件包的哲学是它应仅提供启用多租户功能的基本功能。这就是为什么它只提供两个内置任务。这些任务作为示例实现。
让我们看看这两个任务中的一个:SwitchDatabaseTask
。当您为您的每个租户使用单个数据库时,该任务才有用。
此任务可以切换 tenant
数据库连接配置的数据库名称。将使用的数据库名称将在 Tenant
模型的 database
属性中。
当使用为每个租户单独数据库时,您的 Laravel 应用程序需要两个数据库连接。一个命名为 landlord
,指向包含 tenants
表和其他系统相关信息的数据库。另一个连接,名为 tenant
,指向作为请求当前租户的租户的数据库。
在 database
配置文件中可能会是这样的。
// in config/database.php
'connections' => [
'tenant' => [
'driver' => 'mysql',
'database' => null,
// other options such as host, username, password, ...
],
'landlord' => [
'driver' => 'mysql',
'database' => 'name_of_landlord_db',
// other options such as host, username, password, ...
],
您会发现,tenant
连接的 database
键设置为 null
。当将租户设置为当前租户时,SwitchDatabaseTask
将自动将此 database
键设置为租户的 database
属性中的数据库名称。
这是 SwitchDatabaseTask
的样子。当将租户设置为当前租户时,将调用 makeCurrent
方法。
namespace Spatie\Multitenancy\Tasks;
use Illuminate\Support\Facades\DB;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
use Spatie\Multitenancy\Exceptions\InvalidConfiguration;
use Spatie\Multitenancy\Models\Tenant;
class SwitchTenantDatabaseTask implements SwitchTenantTask
{
use UsesMultitenancyConfig;
public function makeCurrent(Tenant $tenant): void
{
$this->setTenantConnectionDatabaseName($tenant->getDatabaseName());
}
public function forgetCurrent(): void
{
$this->setTenantConnectionDatabaseName(null);
}
protected function setTenantConnectionDatabaseName(?string $databaseName)
{
$tenantConnectionName = $this->tenantDatabaseConnectionName();
if (is_null(config("database.connections.{$tenantConnectionName}"))) {
throw InvalidConfiguration::tenantConnectionDoesNotExist($tenantConnectionName);
}
config([
"database.connections.{$tenantConnectionName}.database" => $databaseName,
]);
DB::purge($tenantConnectionName);
}
}
创建自己的任务很简单。创建任务。任何实现了 Spatie\Multitenancy\Tasks\SwitchTenantTask
的类都是一个任务。以下是如何查看该接口。
namespace Spatie\Multitenancy\Tasks;
use Spatie\Multitenancy\Models\Tenant;
interface SwitchTenantTask
{
public function makeCurrent(Tenant $tenant): void;
public function forgetCurrent(): void;
}
当将租户设置为当前租户时,将调用 makeCurrent
函数。一个常见的是动态更改一些配置值。
创建任务后,您必须通过将类名放入 multitenancy
配置文件的 switch_tenant_tasks
键来注册它。
使 artisan 命令识别租户
如果您想为所有租户执行 artisan 命令,可以使用 tenants:artisan <artisan command>
。此命令将遍历租户,对于每个租户,将执行 artisan 命令并使其成为当前租户。
当租户拥有自己的数据库时,您可以使用此命令迁移每个租户数据库(假设您正在使用类似 SwitchTenantDatabase
的任务)。
php artisan tenants:artisan migrate
总结
我认为我们包的最佳功能是其无边界的特性。通过完全不指定具体内容,并允许包的用户定义切换到租户时的所有所需行为,它保持了一段灵活性。
要了解更多关于我们包的信息,请查看详尽的文档。
我已在上面提到,我还创建了一个不错的视频,演示了如何使用我们的包使Laravel应用程序成为多租户。
如果您希望多租户包具有更多功能,请查看这些优秀的替代方案
我想感谢Mohammed Said。他的多租户视频启发我创建了我们的包。
laravel-multitenancy并不是我和我的团队创建的第一个包。这里有一个包含我们以前开源的所有项目的列表。
fermevc、driesvints、joedixon、ufukayyildiz 赞同了这篇文章