stancl/tenancy 3.x 中文文档 (自用)

没找到中文文档,自己搞了一个,水平有限可能有坑
分段文档:4v2dv542eo.k.topthink.com/@tenant

介绍

什么是多租户?

多租户是指在单个托管应用程序实例中向多个用户(租户)提供服务的能力。这与为每个用户单独部署应用程序形成对比。
multitenantlaravel.com/ 。只需浏览幻灯片,在不到五分钟的时间内就可以获得80%的价值。
请注意,如果您只是想将 todo 任务限定到当前用户,那么无需使用多租户包。只需使用像 auth()->user()->tasks() 这样的调用即可。这是最简单的多租户形式。
这个包的构建基于这样一个理念,即多租户通常意味着让租户拥有自己的用户,这些用户拥有自己的资源,例如todo任务,而不仅仅是用户拥有任务。

多租户的类型

多租户有两种类型:

  • 单数据库租户:租户共享一个数据库,他们的数据是通过 where tenant_id = 1 之类的语句分隔开来的。
  • 多数据库租户:每个租户有自己的数据库。
    这个包可以同时实现这两种方式,但更专注于多数据库租户,因为这种类型需要包的一方进行更多的工作,您的工作量则相对较少。对于单数据库租户,您将获得一个跟踪当前租户和模型特性的类,其余工作则由您自己完成。

多租户的模式

租户模式是这个包的一个独特属性。在以前的版本中,这个包主要是为自动租户设计的,这意味着在识别出租户后,诸如数据库连接、缓存、文件系统、队列等都将切换到该租户的上下文——他的数据完全与其他租户隔离开来。
在当前版本中,我们也将手动租户作为一流特性。我们为您提供了模型特性等功能,如果您希望自己限定数据,则可以使用这些特性。

租户识别

为了使您的应用程序具备租户感知能力,必须确定租户。这个包提供了许多识别中间件类。您可以通过域名、子域名、域名和子域名、路径或请求数据来识别租户。

快速入门教程

本教程旨在快速帮助您开始使用stancl/tenancy 3.x。它实现了多数据库租户和域标识。如果您需要不同的实现方式,这个包绝对可以满足,并且很容易重构为其他实现方式。

我们建议您按照本教程进行操作,以便让您能够使用该包进行尝试。然后,如果有需要,您可以重构多租户实现的细节(例如单数据库租户、请求数据识别等)。

安装

首先,使用composer要求引入包:

composer require stancl/tenancy

然后,运行tenancy:install命令:

php artisan tenancy:install

这将创建一些文件:迁移、配置文件、路由文件和服务提供者。

让我们运行迁移:

php artisan migrate

在config/app.php中注册服务提供者。确保它与下面的代码片段中的位置相同:

/*
 * Application Service Providers...
 */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TenancyServiceProvider::class, // <-- 这里

创建租户模型

现在您需要创建一个租户模型。该包提供了一个默认的租户模型,具有许多功能,但它尝试保持中立,因此我们需要创建一个自定义模型来使用域和数据库。像这样创建app/Models/Tenant.php文件:

<?php

namespace App\Models;

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 HasDatabase, HasDomains;
}

请注意:如果您的模型位于其他位置,您应相应地调整本教程中的代码和命令。

现在,我们需要告诉包使用这个自定义模型。打开config/tenancy.php文件并修改以下行:

'tenant_model' => \App\Models\Tenant::class,

事件

默认设置可以直接使用,但是对事件进行简短的解释会很有用。您的app/Providers目录中的TenancyServiceProvider文件将租户事件映射到侦听器。默认情况下,当创建租户时,它会运行一个JobPipeline(这是该包的一部分),确保按顺序运行CreateDatabase、MigrateDatabase和可选的其他作业(例如SeedDatabase)。

换句话说,它会在创建租户之后创建并迁移租户的数据库 - 并且它会按正确的顺序执行此操作,因为普通的事件侦听器映射会以某种愚蠢的顺序执行侦听器,导致数据库在创建之前被迁移,或者在迁移之前被填充。

中央路由

我们将对app/Providers/RouteServiceProvider.php文件进行一些小的更改。具体来说,我们将确保仅在中央域上注册中央路由。

protected function mapWebRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::middleware('web')
            ->domain($domain)
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));
    }
}

protected function mapApiRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::prefix('api')
            ->domain($domain)
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
}

protected function centralDomains(): array
{
    return config('tenancy.central_domains');
}

在您的RouteServiceProvider的boot()方法中手动调用这些方法,而不是使用$this->routes()方法。

public function boot()
{
    $this->configureRateLimiting();

    $this->routes(function () {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    });
}

中央域

现在我们需要实际指定中央域。中央域是为您的“中央应用程序”内容提供服务的域,例如租户注册的登陆页面。打开config/tenancy.php文件并添加它们:

'central_domains' => [
    'saas.test', // 根据您使用的域名添加。我在使用Laravel Valet时使用此域名。
],

如果使用Laravel Sail,则无需更改,默认值即可:

'central_domains' => [
    '127.0.0.1',
    'localhost',
],

租户路由

默认情况下,您的租户路由如下所示:

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

这些路由仅可在租户(非中央)域上访问 - PreventAccessFromCentralDomains中间件会强制执行此限制。

让我们进行一些小的更改以便能够查看数据库中的所有用户,以便我们实际上可以看到多租户的工作情况。打开routes/tenant.php文件并应用以下修改:

Route::get('/', function () {
    dd(\App\Models\User::all());
    return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
});

迁移

为了在租户数据库中有用户,让我们将users表的迁移(文件database/migrations/2014_10_12_000000_create_users_table.php或类似文件)移动到database/migrations/tenant目录下。这样可以防止在中央数据库中创建该表,而是在创建租户时将其创建在租户数据库中 - 这得益于我们的事件设置。

创建租户

为了测试目的,我们将在tinker中创建一个租户 - 现在不需要浪费时间创建控制器和视图。

$ php artisan tinker
>>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']);
>>> $tenant1->domains()->create(['domain' => 'foo.localhost']);
>>>
>>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']);
>>> $tenant2->domains()->create(['domain' => 'bar.localhost']);

现在我们将在每个租户的数据库中创建一个用户:

App\Models\Tenant::all()->runForEach(function () {
    App\Models\User::factory()->create();
});

试一试

现在,在浏览器中访问foo.localhost,将localhost替换为config/tenancy.php文件中修改的central_domains的值之一。我们应该看到一个用户表的输出,其中包含一些用户。如果我们访问bar.localhost,我们应该看到另一个用户。

安装

需要 Laravel 6.0 或更高版本。

使用composer要求引入包:

composer require stancl/tenancy

然后运行以下命令:

php artisan tenancy:install

这将创建以下内容:

  • 迁移文件
  • 配置文件 config/tenancy.php
  • 路由文件 routes/tenant.php
  • 服务提供者文件 app/Providers/TenancyServiceProvider.php

然后将服务提供者添加到 config/app.php 文件中:

/*
 * Application Service Providers...
 */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TenancyServiceProvider::class, // <-- 在这里

最后,如果您想使用与 .env 文件中的 DB_CONNECTION 定义不同的中央数据库,请将您的中央连接(在 config/database.php 中)命名为 central 或其他任何您想要的名称,但请确保与 tenancy.central_connection 配置中的名称相同。

配置

该程序包具有高度可配置性。本页介绍了您可以在 config/tenancy.php 文件中配置的内容,但请注意,还有许多其他可配置的内容。有些内容可以通过扩展类(例如 Tenant 模型)进行更改,而其他内容可以使用静态属性进行更改。通常情况下,这些内容将在文档的相应页面中提到,但并非每次都会提到。因此,不要害怕深入研究该程序包的源代码-每当您使用的类具有 public static 属性时,它都可以进行配置。

静态属性

您可以通过以下方式设置静态属性(示例):

\Stancl\Tenancy\Middleware\InitializeTenancyByDomain::$onFail = function () {
    return redirect('https://my-central-domain.com/');
};

放置这些调用的好地方是您的 app/Providers/TenancyServiceProviderboot() 方法。

租户模型

tenancy.tenant_model

该配置指定包应使用的 Tenant 模型。很有可能您正在使用自定义模型,正如在 Tenants 页面中指示的那样,请确保在配置中进行更改。

唯一 ID 生成器

tenancy.id_generator

默认情况下,应用于默认的 Tenant 模型的 Stancl\Tenancy\Database\Concerns\GeneratesIds trait 将生成一个唯一的 ID(默认为 UUID),如果没有提供租户 ID。

如果您希望使用自增 ID 而不是 UUID:

  1. 将此配置键设置为 null,或创建一个不使用此 trait 的自定义租户模型
  2. 更新 tenants 表迁移以使用递增列类型,而不是 string
  3. 更新 domains 表迁移的 tenant_id 列与 tenants id 相同的类型

域模型

tenancy.domain_model

与租户模型配置类似。如果您正在使用自定义域模型,请在此配置中进行更改。如果根本不使用域(例如,使用路径或请求数据进行标识),请忽略此配置键。

中央域名

tenancy.central_domains

域名列表,托管您的 中央应用。这在以下情况下使用:

  • PreventAccessFromCentralDomains 中间件,以防止从中央域名访问租户路由
  • InitializeTenancyBySubdomain 中间件,检查当前主机名是否是其中一个中央域名的子域名

启动器

tenancy.bootstrappers

此配置数组允许您启用、禁用或添加自己的 租户启动器

数据库

如果您使用的是 Laravel Sail,请参阅 Laravel Sail 集成指南

此部分与多数据库租户相关,具体而言是与 DatabaseTenancyBootstrapper 和管理租户数据库的逻辑相关。

请在配置中查看此部分,有附有注释的文档。

缓存

tenancy.cache.*

此部分与缓存分离相关,具体而言是与 CacheTenancyBootstrapper 相关。

注意:要使用缓存分离,您需要使用支持标记的缓存存储,通常是 Redis。

请在配置中查看此部分,有附有注释的文档。

文件系统

tenancy.filesystem.*

此部分与存储分离相关,具体而言是与 FilesystemTenancyBootstrapper 相关。

请在配置中查看此部分,有附有注释的文档。

Redis

tenancy.redis.*

此部分与 Redis 数据分离相关,具体而言是与 RedisTenancyBootstrapper 相关。

注意:要使用此启动器,您需要安装 phpredis。

请在配置中查看此部分,有附有注释的文档。

功能

tenancy.features

此配置数组允许您启用、禁用或添加自己的 功能类

迁移参数

tenancy.migration_parameters

此配置数组允许您在运行 tenants:migrate 命令时(或者使用 MigrateDatabase 作业执行此命令时)设置默认参数。当然,所有这些参数都可以通过直接在命令调用中传递它们来覆盖,无论是在 CLI 中还是使用 Artisan::call()

Seeder 参数

tenancy.seeder_parameters

与迁移参数相同,但适用于 tenants:seedSeedDatabase 作业。

与其他包的比较

hyn/multi-tenancy

该包旨在为您的应用程序手动添加多租户所需的工具。它提供了模型 traits、用于创建租户数据库的类以及一些其他工具。

这是一个不错的选择,如果您想要手动实现多租户,但是:

  • 它没有在过去一年左右的时间里加入任何新功能,不再积极开发。
  • 在测试方面存在问题。最近几个月,我收到了以下反馈:

但是,我在 Hyn 中无法运行任何测试,还遇到了一些排队问题,仍然对此感到紧张。
目前,我们的应用正在使用最新的 Laravel 和最新的 hyn/tenancy。我不喜欢的唯一一件事是,测试非常脆弱,以至于我不敢随便更改任何内容,以免造成一切崩溃。
顺便说一下,这个包非常棒!它比我认为有点混乱的 hyn 备选方案好太多了… 很遗憾我一开始没有找到它。

我并不是故意说 hyn/multi-tenancy 不好,但是如果您决定使用该包,一定要非常小心。

tenancy/tenancy

该包旨在为构建自己的多租户实现提供框架。文档相对较少,所以我无法获得太详细的了解,但据我了解,它提供了一些事件,您可以利用这些事件来构建自己的多租户逻辑。

如果您希望绝对灵活,并且本来会构建自己的实现,那么研究一下这个包可能会有所帮助。

然而,如果您希望快速创建一个多租户项目,那么这可能不是正确的选择。

spatie/laravel-multitenancy

该包是一个非常简单的多租户实现。

它与 stancl/tenancy v2 的功能相同,但开箱即用的功能要少得多。

与 stancl/tenancy v2 相比,我唯一看到的好处就是它开箱即用支持 Eloquent,这使得集成像 Cashier 这样的功能更加容易。但是,由于我们已经处于 v3,v3 已经使用了 Eloquent,所以这点不重要。

因此,我建议只有在您出于某种原因重视简单性,并且不会构建任何复杂度和对“业务功能”有需求的东西时,才考虑使用该包。

stancl/tenancy

在我(当然是有偏见的,但也很可能是真实的)的观点中,对于绝大多数应用程序来说,这个包是绝佳的选择。

实际上,我唯一真正考虑的包是我的(当然了),以及如果您需要非常定制的东西,那么可以考虑 tenancy/tenancy。尽管在 99% 的应用程序中,我不认为有必要去选择 tenancy/tenancy。

这个包试图与 tenancy/tenancy 一样灵活,并提供大量开箱即用的功能和其他工具。它在 v3 中继续使用自动方法添加了许多功能,其中大部分都是“企业”功能。

以下是一个不完整但足够好的功能列表:

  • 多数据库多租户

    • 创建数据库
      • MySQL
      • PostgreSQL
      • PostgreSQL(模式模式)
      • SQLite
    • 创建数据库用户
    • 自动切换数据库
    • CLI 命令,比如 spatie/laravel-multitenancy 更多功能
      • migrate
      • migrate:fresh
      • seed
  • 单数据库多租户

    • 具有全局作用域的模型 traits
  • 丰富的事件系统

  • 非常高的可测试性

  • 自动多租户

    • 多租户引导程序可以切换:
      • 数据库连接
      • Redis 连接
      • 缓存标签
      • 文件系统根目录
      • 队列上下文
  • 手动多租户

    • 模型 traits
  • 开箱即用的租户识别

    • 域名识别
    • 子域名识别
    • 路径识别
    • 请求数据识别
    • 用于上述方法的中间件类
    • CLI 参数识别
    • 手动识别(例如在 tinker 中)
  • 与许多包的集成

    • spatie/laravel-medialibrary
    • spatie/laravel-activitylog
    • Livewire
    • Laravel Nova
      • 用于管理租户
      • 用于在租户应用程序中使用
    • Laravel Horizon
    • Laravel Telescope
    • Laravel Passport
  • 在多个租户数据库之间同步用户(或任何其他数据库资源)

  • 当前租户的依赖注入

  • 租户用户模拟

  • 适用于所有租户解析器的缓存租户查找

    两个应用程序

在本文档中,您会经常遇到以下两个术语:

  • 中央应用程序
  • 租户应用程序

这些术语指的是您的应用程序中承载中央逻辑和租户逻辑的部分。

租户应用程序在租户上下文中执行 —— 通常使用租户的数据库、缓存等。当没有租户时,执行中央应用程序。

中央应用程序将包含您的注册页面(用于创建租户)、用于管理租户的管理员面板等。

租户应用程序可能会承载您应用程序的大部分内容 —— 租户真正使用的服务。

租户

一个租户可以是任何实现了 Stancl\Tenancy\Contracts\Tenant 接口的模型。

该包带有一个基本的 Tenant 模型,该模型已经准备好用于常见的事情,不过在大多数情况下,您需要进行扩展,以使其不过于主观。

基本模型在必要接口的基础上具有以下功能:

  • 强制中央连接(允许在租户上下文中与 Tenant 模型进行交互)
  • 数据列 trait — 允许您存储任意键。在您的 tenants 表上不存在的属性作为序列化的 JSON 存储在 data 列中。
  • Id 生成 trait — 当您不提供 ID 时,将生成一个随机 UUID。使用 AUTOINCREMENT 列的替代方法是。如果您希望使用数字 ID,请更改 create_tenants_table 迁移,使用 bigIncrements() 或类似的列类型,并将 tenancy.id_generator 配置设置为 null。这将完全禁用 ID 生成,并回退到数据库的自增机制。

租户模型

大多数使用此包的应用程序都希望有领域/子域标识和租户数据库。为此,请创建一个新模型,例如 App\Tenant,例如:

<?php

namespace App;

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 HasDatabase, HasDomains;
}

然后,在 config/tenancy.php 中配置该包使用此模型:

'tenant_model' => \App\Tenant::class,

如果您想自定义 Domain 模型,您也可以这样做。

如果您不需要域名或数据库,请忽略上述步骤。 一切都将正常工作。

创建租户

您可以像创建其他模型一样创建租户:

$tenant = Tenant::create([
    'plan' => 'free',
]);

创建租户后,将会触发一个事件。这将导致诸如创建和迁移数据库等操作,具体取决于监听事件的作业。

自定义列

租户模型的属性,没有自己的列将保存在 data JSON 列中。您可以像设置普通模型属性一样设置这些属性:

$tenant->update([
    'attributeThatHasNoColumn' => 'value', // 存储在 `data` JSON 列中
    'plan' => 'free' // 存储在专用的 `plan` 列中(见下文)
]);

或者

$tenant->customAttribute = 'value'; // 存储在 `data` JSON 列中
$tenant->plan = 'free'; // 存储在 `plan` 列中(见下文)
$tenant->save();

您可以通过在租户模型上重写 getCustomColumns() 方法来定义自定义列(不会 存储在 data JSON 列中):

public static function getCustomColumns(): array
{
    return [
        'id',
        'plan',
    ];
}

不要忘记在自定义列中保留 id

如果要重命名 data 列,请在迁移中重命名它,并在模型上实现此方法:

public static function getDataColumn(): string
{
    return 'my-data-column';
}

请注意,使用 where() 查询 data 列中的数据将需要这样做:

where('data->foo', 'bar')

数据列只在模型检索和保存时进行编码/解码。

此外,一个良好的经验法则是,当您需要使用 WHERE 子句查询数据时,应该有一个专用的列。这将提高性能,并且您不必考虑 data-> 前缀。

在租户上下文中运行命令

您可以在租户的上下文中运行命令,然后通过将可调用对象传递给租户对象的 run() 方法返回到先前的上下文(无论是中央还是另一个租户)。

例如:

$tenant->run(function () {
    User::create(...);
});

内部键

以内部前缀(默认为 tenancy_,但您可以通过重写 internalPrefix() 方法来自定义)开头的键是供内部使用的,因此不要使用这些前缀作为属性/列名称。

事件

Tenant 模型分发 Eloquent 事件,所有事件都有各自的类。您可以在事件系统页面阅读更多信息。

访问当前租户

您可以使用 tenant() 辅助函数访问当前租户。您还可以传递参数从该租户模型获取属性,例如 tenant('id')

或者,您可以使用 Stancl\Tenancy\Contracts\Tenant 接口进行类型提示,以通过服务容器注入模型。

自增 ID

默认情况下,迁移对 id 列使用 string,并且在创建租户时,模型会生成 UUID,当您不提供 id 时。

如果您希望改为使用递增 ID,可以重写 getIncrementing() 方法:

public function getIncrementing()
{
    return true;
}

域名

注意:域名是可选的。如果您使用路径或请求数据标识,您不需要担心它们。

要将域名添加到租户中,请使用 domains 关联关系:

$tenant->domains()->create([
    'domain' => 'acme',
]);

如果您使用子域名标识中间件,上述示例将适用于 acme.{您的任何中央域名}。如果您使用域名标识中间件,请使用完整的主机名,例如 acme.com。如果您使用组合的域名/子域名标识中间件,您应该同时使用 acme 作为子域名和 acme.com 作为域名。

请注意,从 Laravel 8 开始,Laravel TrustHost 中间件默认启用(请参阅 laravel/laravel#5477)。此中间件会阻止基于域名的租户标识,因为这些请求将被视为“不受信任”,除非将其添加为受信任的主机。您可以在 App\Http\Kernel.php 中将此中间件注释掉,或者您可以在 App\Http\Middleware\TrustHosts.phphosts() 方法中添加自定义的租户域名。

要检索当前域名模型(使用域名标识时),您可以访问 DomainTenantResolver 上的 $currentDomain 公共静态属性。

本地开发

对于本地开发,您可以使用 *.localhost 域名(例如 foo.localhost)作为租户。在许多操作系统上,这些域名的工作方式与 localhost 相同。

如果您使用 Valet,您可能希望将 saas.test 用作中央域名,将 foo.saas.testbar.saas.test 等用作租户域名。或者,如果您想要使用 多个二级域名,您可以使用 valet link 命令将其他域名附加到项目中。例如:valet link bar.test

事件系统

这个包非常依赖事件,使得它非常灵活。

默认情况下,事件的配置方式使得该包的工作方式如下:

  • 接收到一个租户路由的请求,并经过一个识别中间件
  • 识别中间件找到正确的租户并执行
$this->tenancy->initialize($tenant);
  • Stancl\Tenancy\Tenancy 类将 $tenant 设置为当前租户,并触发一个 TenancyInitialized 事件

  • BootstrapTenancy 类捕获事件并执行被称为 tenancy bootstrappers 的类

  • tenancy bootstrappers 对应用程序进行更改,以使其“scoped”到当前租户。默认情况下,这些更改包括:

    • 切换数据库连接
    • 替换 CacheManager 为作用域缓存管理器
    • 添加文件系统路径后缀
    • 在处理队列时存储租户ID并初始化租户

再次强调,上述所有内容都是可配置的。您甚至可以禁用所有的 tenancy bootstrappers,只使用租户识别,并手动将应用程序围绕存储在 Stancl\Tenancy\Tenancy 中的租户进行处理。这完全取决于您的选择。

TenancyServiceProvider

安装该包后,它将为您的应用程序添加一个非常方便的服务提供者。此服务提供者用于将监听器映射到该包特定的事件,并且是您应该放置任何与租户相关的服务容器调用的地方,以避免污染 AppServiceProvider

请注意,您可以在任何您想要的地方注册该包事件的监听器。服务提供者中的事件/监听器映射仅仅是为了方便您的生活。如果您想要手动注册监听器,就像下面的示例一样,您也可以这样做。

Event::listen(TenancyInitialized::class, BootstrapTenancy::class);

启动租户

默认情况下,BootstrapTenancy 类会监听 TenancyInitialized 事件(与上面的示例中所示完全相同)。该监听器将执行配置的 tenancy bootstrappers,将应用程序转换为租户的上下文。您可以在 tenancy bootstrappers 页面上了解更多信息。

相反,当触发 TenancyEnded 事件时,RevertToCentralContext 事件将应用程序转换回中央上下文。

作业流水线

即使在不使用该包的项目中,您可能仍然想要使用作业流水线 —— 我认为这是一个很酷的概念,所以它们被提取到了一个单独的包中:github.com/stancl/jobpipeline

JobPipeline 是一个简单但极其强大的类,它允许您将任何(一系列)作业转换为事件监听器。

您可以像使用其他监听器一样使用作业流水线,因此可以在 TenancyServiceProviderEventServiceProvider 使用 $listen 数组,或者在任何其他地方使用 Event::listen() 进行注册。

创建作业流水线

要创建一个作业流水线,首先指定要使用的作业:

use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};

JobPipeline::make([
    CreateDatabase::class,
    MigrateDatabase::class,
    SeedDatabase::class,
])

然后,指定要传递给作业的变量。这通常来自事件。

use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
use Stancl\Tenancy\Events\TenantCreated;

JobPipeline::make([
    CreateDatabase::class,
    MigrateDatabase::class,
    SeedDatabase::class,
])->send(function (TenantCreated $event) {
    return $event->tenant;
})

接下来,决定是否要将流水线排入队列。默认情况下,流水线是同步的(不排队)。

如果您想要默认情况下将流水线排队,请设置一个静态属性:\Stancl\JobPipeline\JobPipeline::$shouldBeQueuedByDefault = true;

use Stancl\Tenancy\Events\TenantCreated;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};

JobPipeline::make([
    CreateDatabase::class,
    MigrateDatabase::class,
    SeedDatabase::class,
])->send(function (TenantCreated $event) {
    return $event->tenant;
})->shouldBeQueued(true),

最后,将流水线转换为监听器并将其绑定到事件:

use Stancl\Tenancy\Events\TenantCreated;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Jobs\{CreateDatabase, MigrateDatabase, SeedDatabase};
use Illuminate\Support\Facades\Event;

Event::listen(TenantCreated::class, JobPipeline::make([
    CreateDatabase::class,
    MigrateDatabase::class,
    SeedDatabase::class,
])->send(function (TenantCreated $event) {
    return $event->tenant;
})->shouldBeQueued(true)->toListener());

请注意,您甚至可以使用作业流水线将单个作业转换为事件监听器。如果作业类中有一些逻辑,并且不想为了能够在事件触发时运行这些作业而创建监听器类,那么这将非常有用。

可用事件

注意:某些数据库事件(DatabaseMigratedDatabaseSeededDatabaseRolledback 及可能其他事件)是在租户上下文中触发的。根据应用程序如何引导租户,您可能需要在这些事件的监听器中明确与中央数据库进行交互 —— 如果需要的话。

注意:所有事件位于 Stancl\Tenancy\Events 命名空间。

Tenancy 事件

  • InitializingTenancy
  • TenancyInitialized
  • EndingTenancy
  • TenancyEnded
  • BootstrappingTenancy
  • TenancyBootstrapped
  • RevertingToCentralContext
  • RevertedToCentralContext

请注意,初始化租户和引导租户之间的区别。当将租户加载到 Tenancy 对象中时,租户被初始化。而引导则是在初始化之后进行的 —— 如果您正在使用自动租户,BootstrapTenancy 类正在监听 TenancyInitialized 事件,并在执行完引导程序后触发一个事件,表示租户已经引导完毕。如果要在应用程序转换为租户上下文后执行某些操作,请使用引导完毕的事件。

Tenant 事件

以下事件是作为默认的 Tenant 实现中 Eloquent 事件的结果进行分发的(最常用的事件已加粗):

  • CreatingTenant
  • TenantCreated
  • SavingTenant
  • TenantSaved
  • UpdatingTenant
  • TenantUpdated
  • DeletingTenant
  • TenantDeleted

Domain 事件

这些事件是可选的。只有在使用域名作为租户时才与您相关。

  • CreatingDomain
  • DomainCreated
  • SavingDomain
  • DomainSaved
  • UpdatingDomain
  • DomainUpdated
  • DeletingDomain
  • DomainDeleted

Database 事件

这些事件也是可选的。只有在使用多数据库租户时才与您相关。

  • CreatingDatabase
  • DatabaseCreated
  • MigratingDatabase
  • DatabaseMigrated
  • SeedingDatabase
  • DatabaseSeeded
  • RollingBackDatabase
  • DatabaseRolledBack
  • DeletingDatabase
  • DatabaseDeleted

资源同步

  • SyncedResourceSaved
  • SyncedResourceChangedInForeignDatabase

    路由

该包有一个中央路由和租户路由的概念。中央路由仅在中央域上可用,租户路由仅在租户域上可用。如果您不使用域标识,则所有路由始终可用,您可以跳过关于阻止其他域名访问的细节。

中央路由

您可以像往常一样在routes/web.phproutes/api.php中注册中央路由。但是,您需要对您的RouteServiceProvider进行一些小的更改。

您不希望中央路由(例如着陆页和注册表单)在租户域上可访问。因此,以仅在中央域上访问的方式注册它们:

// RouteServiceProvider

protected function mapWebRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::middleware('web')
            ->domain($domain)
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));
    }
}

protected function mapApiRoutes()
{
    foreach ($this->centralDomains() as $domain) {
        Route::prefix('api')
            ->domain($domain)
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
}

protected function centralDomains(): array
{
    return config('tenancy.central_domains', []);
}

注意:如果您使用多个中央域,您不能使用路由名称,因为不同的路由(不同的域名和路径组合)不能共享相同的名称。如果您需要在测试中使用不同的中央域,请在您的测试用例的setUp()方法中使用config()->set()

租户路由

您可以在routes/tenant.php中注册租户路由。这些路由没有应用任何中间件,并且它们的控制器命名空间在app/Providers/TenancyServiceProvider中指定。

默认情况下,您会看到以下设置:

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

此组中的路由将应用web中间件组、初始化中间件和相关中间件。您可以对api路由组执行相同的操作。

您还可以使用与域名中间件不同的初始化中间件。完整列表请参阅租户识别页面。

冲突的路径

由于服务提供程序(及其路由)的注册顺序,租户路由将优先于中央路由。因此,如果您在routes/web.php文件中有一个/路由,但还有routes/tenant.php,租户路由将在租户域上使用。

但是,那些没有中央对应路由的租户路由仍然可在中央域上访问,并将导致“无法在域…上识别租户”的错误。为了避免这种情况,在所有租户路由上使用Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains中间件。如果用户试图在中央域上访问租户路由,则此中间件将中止并返回404。

通用路由

请参阅通用路由功能

Tenancy启动程序

Tenancy启动程序是一种使您的应用程序能够感知租户的方式,而无需更改代码的一行,但是事情将限定在当前租户范围内。

该包内置了以下启动程序:

数据库Tenancy启动程序

数据库Tenancy启动程序在构建租户的连接后,将默认数据库连接切换到“tenant”。

自定义数据库

请注意,仅切换了“默认”连接。如果您明确使用其他连接,无论是使用DB::connection('...')、模型的getConnectionName()方法,还是使用CentralConnection模型特性,都会被尊重。启动程序不会“强制”任何连接,它只是切换默认连接。

缓存Tenancy启动程序

缓存Tenancy启动程序用当前租户的标识添加标签到每个缓存调用,从而将缓存调用限定在特定租户范围内,您可以选择清除特定租户的缓存:

php artisan cache:clear --tag=tenant_123

请注意,您必须使用支持标签的缓存存储,例如Redis。

文件系统Tenancy启动程序

文件系统启动程序通过修改Storage门面、storage_path()asset()辅助函数返回的路径,使您的应用程序的文件系统功能也能感知租户。

注意:如果您想以不同的方式引导文件系统Tenancy(例如,为每个租户提供一个S3存储桶),您完全可以这样做。查看包的启动程序以了解如何编写自定义启动程序的示例,并随意根据您的需求进行实现。

storage_path()辅助函数

启动程序会在storage_path()返回的路径后添加后缀,以使该辅助函数能够感知租户。

  • 后缀的构建是通过将租户键附加到您的suffix_base。默认情况下,suffix_basetenant,但您可以在tenancy.filesystem配置中自由更改它。例如,如果租户的键是42suffix_basetenant,那么后缀将是tenant42

  • 经过后缀处理后,storage_path()辅助函数将返回"/$path_to_your_application/storage/tenant42/"

因为storage_path()会被添加后缀,所以您的文件夹结构将如下所示:

├── app/
├── bootstrap/
├── config/
├── database/
├── public/
├── ...
└── storage/
     ├── .gitignore
     ├── app/
     ├── framework/
     ├── logs/
     ├── tenant42/
     └── ...

请注意,日志将保存在storage/logs目录中,与storage_path()的任何更改和租户无关。

Storage门面

启动程序还会通过在config('tenancy.filesystem.disks')中列出的磁盘根目录后添加后缀,并通过在config('tenancy.filesystem.root_override')中覆盖磁盘根目录(磁盘根目录是Storage门面使用的磁盘路径)来使Storage门面感知租户。

列在config('tenancy.filesystem.disks')中的每个磁盘的根目录将被添加后缀。仅此操作可能会导致意外的行为,因为Laravel会对路径进行自己的后缀处理,所以文件系统配置中有root_override部分,它允许您在Tenancy初始化之后覆盖磁盘根目录:

// Tenancy配置(tenancy.filesystem.root_override)
// %storage_path%将被storage_path()的输出替换
// 例如,Storage::disk('local')->path('')将返回 "/$path_to_your_application/storage/tenant42/app"
// (假设后缀基础为'tenant',租户的键为'42'。与上面的Storage路径助手部分示例相同)
'root_override' => [
    'local' => '%storage_path%/app/',
    'public' => '%storage_path%/app/public/',
],

要使租户感知的Storage门面与自定义磁盘一起工作,请将磁盘名称添加到config('tenancy.filesystem.disks')中,并且如果磁盘是本地的,则像上述示例中所示,在config('tenancy.filesystem.root_override')中覆盖其根目录。对于S3,不需要覆盖磁盘根目录 - Storage::disk('s3')->path('foo.txt')将返回/tenant42/foo.txt

资源文件

文件系统启动程序通过修改asset()辅助函数的行为,使其链接到“当前租户”的文件。默认情况下,启动程序使辅助函数输出一个指向TenantAssetsController(/tenancy/assets/...)的URL,TenantAssetsController返回一个文件响应:

// TenantAssetsController
return response()->file(storage_path('app/public/' . $path));

该包期望将资源文件存储在租户的app/public/目录中。对于全局资源(在所有租户之间共享的非私有资源),您可以创建一个磁盘并使用该磁盘的URL。例如:

Storage::disk('branding')->url('header-logo.png');

要访问全局资源(如JS/CSS资源),您可以使用global_asset()mix()

配置资源URL(在您的.env中的ASSET_URL)将更改asset()辅助函数的行为 - 当设置了资源URL时,启动程序会对配置的资源URL进行后缀处理(与storage_path()的处理方式相同),并使asset()辅助函数输出该URL,而不是指向TenantAssetsController的路径。

您可以在配置中禁用asset()的租户功能(tenancy.filesystem.asset_helper_tenancy),并明确使用tenant_asset()tenant_asset()始终返回指向TenantAssetController的路径:tenant_asset('foo.txt')返回your-site.com/tenancy/assets/foo.txt。如果您在租户应用程序中使用了使用asset()的包,您可能希望这样做。

在使用asset()辅助函数之前,请确保将您的应用程序中使用的标识中间件分配给TenantAssetsController的$tenancyMiddleware

// TenancyServiceProvider(不要忘了导入类)
public function boot()
{
    // 更新资源控制器使用的中间件
    TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomainOrSubdomain::class;
}

队列Tenancy启动程序

此启动程序将当前租户的ID添加到排队的作业有效负载中,并在处理作业时根据此ID初始化Tenancy。

启动程序具有一个静态的$forceRefresh属性,默认值为false。将该属性设置为true将使每个排队的作业重新初始化Tenancy。当您更改租户的状态(例如data列中的属性)并希望下一个作业再次使用新数据初始化Tenancy时,这非常有用。仅当初始化Tenancy时才会执行诸如Tenant Config等功能,因此在某些情况下需要重新初始化。

有关此功能的更多信息,请阅读“队列”页面。

Redis Tenancy启动程序

如果您在租户应用程序中使用Redis调用(不是缓存调用,而是直接的Redis调用),您将希望限定Redis数据的范围。为此,请使用此启动程序。它会为每个租户更改Redis前缀。

请注意,您需要phpredis,predis不起作用。

编写自定义启动程序

如果要为尚未包括在此包中的内容引导Tenancy(或者包括在此包中的内容,但您希望有不同的行为),您可以创建一个启动程序类。

该类必须实现Stancl\Tenancy\Contracts\TenancyBootstrapper接口:

namespace App;

use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;

class MyBootstrapper implements TenancyBootstrapper
{
    public function bootstrap(Tenant $tenant)
    {
        // ...
    }

    public function revert()
    {
        // ...
    }
}

然后,在tenancy.bootstrappers配置中注册它:

'bootstrappers' => [
    Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
    Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
    Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
    Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,

    App\MyBootstrapper::class,
],

可选功能

“功能”是提供额外功能的类,这些功能对于核心租户逻辑而言并不是必需的。该包内置了以下功能:

  • UserImpersonation:为租户的数据库中的用户生成模拟令牌,以便在其他上下文中使用。

  • TelescopeTags:为Telescope的记录添加带有当前租户ID的标签。

  • TenantConfig:将租户存储中的键映射到应用程序配置中。

  • CrossDomainRedirect:在RedirectResponse上添加domain()宏,使您可以更改生成路由的预期主机名。

  • UniversalRoutes:适用于中央和租户上下文中均有效的路由操作。

  • ViteBundler:使Vite生成正确的资源路径。

所有功能类都位于Stancl\Tenancy\Features命名空间下。

您可以通过将功能类名添加到tenancy.features配置中来注册功能。

用户模拟

该包提供了一个功能,允许您在租户数据库中模拟用户。该功能适用于任何识别方法任何有状态的身份验证守卫,即使您使用多个。

注意:如果您目前正在使用非有状态的身份验证守卫(例如 Laravel Sanctum 的守卫),您仍然可以通过将有状态守卫传递给tenancy()->impersonate()(例如 'web' 守卫)来使用用户模拟。

工作原理

您生成一个模拟令牌并将其存储在中央数据库的tenant_user_impersonation_tokens表中。

表中的每个记录保存以下数据:

  • 令牌值(128个字符的字符串)
  • 租户的ID
  • 用户的ID
  • 身份验证守卫的名称
  • 模拟完成后重定向的URL

您访问您创建的模拟路由 - 尽管您只需要做一些少量的工作,您的路由主要是调用功能提供的方法。该路由是一个租户路由,这意味着如果您使用域名识别,则位于租户域上,如果使用路径识别,则以租户ID作为前缀。

该路由尝试根据令牌在表中查找记录,如果有效,则使用存储的用户ID对身份验证守卫进行身份验证,并将您重定向到存储的URL。

如果模拟成功,则从数据库中删除令牌。

默认情况下,所有的令牌在60秒后过期,可以自定义此TTL —— 请参见底部的部分。

启用该功能

要启用此功能,请打开您的config/tenancy.php文件,并确保以下类在配置文件的features部分中:

Stancl\Tenancy\Features\UserImpersonation::class,

接下来,发布创建包含模拟令牌表的迁移:

php artisan vendor:publish --tag=impersonation-migrations

最后,运行迁移:

php artisan migrate

使用方法

首先,您需要创建一个类似下面的租户路由:

use Stancl\Tenancy\Features\UserImpersonation;

// 这是您的租户路由!

Route::get('/impersonate/{token}', function ($token) {
    return UserImpersonation::makeResponse($token);
});

请注意,路由的路径或名称完全由您决定。包的唯一逻辑是生成令牌,验证令牌,并模拟用户登录。

然后,在您的应用程序中使用模拟功能时,可以这样生成一个令牌:

// 假设我们希望在模拟用户登录后重定向到仪表盘。
$redirectUrl = '/dashboard';

$token = tenancy()->impersonate($tenant, $user->id, $redirectUrl);

然后将用户(或可能是“管理员”)重定向到您创建的路由。

域名识别

// 注意:这不是包的一部分,您需要根据需要自己实现“主要域”的概念。或者如果您在每个租户上使用一个域名,该包允许您随意使用任何方式。

$domain = $tenant->primary_domain;
return redirect("https://$domain/impersonate/{$token->token}");

路径识别

// 确保在您的路由中使用正确的前缀。
return redirect("{$tenant->id}/impersonate/{$token->token}");

就是这样。用户将被重定向到您的模拟路由,以模拟用户身份登录,最后重定向到您指定的重定向URL。

自定义身份验证守卫

注意:用户模拟使用的身份验证守卫必须是有状态的(即实现了Illuminate\Contracts\Auth\StatefulGuard接口)。

如果您正在使用多个身份验证守卫,您可能希望指定模拟逻辑应使用的身份验证守卫。

要做到这一点,只需将身份验证守卫名称作为第四个参数传递给impersonate()方法。因此,根据上面的示例,我们可以扩展如下:

tenancy()->impersonate($tenant, $user->id, $redirectUrl, 'jwt');

自定义

您可以通过将下列静态属性设置为您想要的秒数来自定义模拟令牌的TTL:

Stancl\Tenancy\Features\UserImpersonation::$ttl = 120; // 2分钟

Telescope标签

Laravel Telescope 提供了有关进入应用程序的请求的洞察。您可以通过标签对这些请求进行过滤。启用Telescope标签功能后,您可以自动对所有活动租户的请求进行标记。

启用该功能

请取消注释tenancy.features配置文件中的以下行:

// Stancl\Tenancy\Features\TelescopeTags::class,

租户配置

在您的应用程序中,您可能需要使用与租户相关的配置。这些配置可以是API密钥,例如”每页产品”等等。

您可以直接使用租户模型来获取这些值,但是您可能仍然希望使用Laravel的config()函数,因为:

  • 关注点分离 - 如果您只写一个与租户实现无关的config('shop.products_per_page'),那么在更改租户实现时会更容易。
  • 默认值 - 您可能只想使用租户存储来覆盖配置文件中的某些值。

启用该功能

请取消注释tenancy.features配置文件中的以下行:

// Stancl\Tenancy\Features\TenantConfig::class,

配置映射

该功能将租户存储中的键与配置键进行映射,映射关系基于$storageToConfigMap的静态属性。举个例子,如果您的$storageToConfigMap如下所示:

\Stancl\Tenancy\Features\TenantConfig::$storageToConfigMap = [
    'paypal_api_key' => 'services.paypal.api_key',
];

当租户初始化时,租户模型中的paypal_api_key值将被复制到services.paypal.api_key配置中。

将值映射到多个配置键

有时,您可能希望将值复制到多个配置键。为此,请将配置键指定为数组:

\Stancl\Tenancy\Features\TenantConfig::$storageToConfigMap = [
    'locale' => [
        'app.locale',
        'locales.default',
    ],
];

跨域重定向

要启用此功能,请取消注释tenancy.features配置文件中的以下行:Stancl\Tenancy\Features\CrossDomainRedirect::class

有时,您可能希望将用户重定向到不同域上的特定路由(而不是当前域)。假设您希望在租户注册后将其重定向到其域上的home路径:

return redirect()->route('home')->domain($domain);

您还可以使用tenant_route()助手函数将用户重定向到另一个域。

return redirect(tenant_route($domain, 'home'));

通用路由

注意:如果您需要自定义onFail逻辑,您不能使用此功能,因为它会覆盖您对该逻辑的任何更改。相反,可以查看此功能的源代码,并使您的onFail逻辑也实现通用路由。如果您这样做,请确保在配置文件中禁用此功能,然后清除缓存。

有时,您可能希望在中央应用程序和租户应用程序中使用完全相同的路由动作。请注意,重点在于路由action - 您可以在中央和租户路由中使用相同的path但使用不同的动作,而本节介绍了使用相同的路由和动作。

通常情况下,尽量避免尽可能使用这些用例,并优先复制代码。在中央和租户应用程序中使用相同的控制器和模型将在您需要稍有不同行为时崩溃 - 例如,由控制器返回不同的视图,模型中的不同行为等。

首先,通过取消注释以下行来启用UniversalRoutes功能,将其添加到您的tenancy.features配置文件中:

Stancl\Tenancy\Features\UniversalRoutes::class,

然后,转到您的app/Http/Kernel.php文件并添加以下中间件组:

'universal' => [],

我们将使用此中间件组作为路由的”标志”,将其标记为通用路由。组内不需要任何实际中间件。

然后,创建以下路由:

Route::get('/foo', function () {
    // ...
})->middleware(['universal', InitializeTenancyByDomain::class]);

该路由将在中央和租户应用程序中都工作。如果找到租户,则将初始化租户。否则,将使用中央上下文。

如果您使用了不同的中间件,请查看UniversalRoutes功能的源代码,并相应更改公共静态属性。

Vite打包工具

启用ViteBundler功能可以通过使用global_asset()辅助函数而不是默认的asset()辅助函数,使Vite生成正确的资产路径。

要启用此功能,请取消注释tenancy配置文件中features部分中的Stancl\Tenancy\Features\ViteBundler::class

'features' => [
    // [...]
    Stancl\Tenancy\Features\ViteBundler::class,
],

自动模式

默认情况下,该软件包会在后台自动引导多租户。这意味着当识别到一个租户时(通常使用中间件),默认的数据库/缓存/文件系统等会切换到该租户的上下文。您可以在Tenancy bootstrappers页面上了解更多信息。

工作流程如下:

触发TenancyInitialized事件 → BootstrapTenancy监听 → 执行多租户引导程序

建议使用此模式,因为:

  • 关注点分离。多租户在应用程序的下一层发生。如果您需要更改引导多租户的详细信息,可以在不更改大量应用程序代码的情况下进行更改。
  • 在编写应用程序代码时,无需考虑多租户工作的内部细节。当您编写应用程序的租户部分时,您只是在编写例如电子商务应用程序,而不是多租户电子商务应用程序。在编写验证规则时无需考虑数据库连接。
  • 与其他软件包的良好集成。切换默认数据库连接(和其他内容)是将许多软件包集成到应用程序的租户部分的唯一方法。例如,您可以使用Laravel Nova来管理租户应用程序中的资源。

    手动模式

请参阅:自动模式

如果您希望仅使用该软件包来跟踪当前租户并手动使应用程序具备多租户功能,而不使用Tenancy bootstrappers,您完全可以这样做。

您可以使用Stancl\Tenancy\Database\Concerns\CentralConnectionStancl\Tenancy\Database\Concerns\TenantConnection模型 trait,来使模型明确地使用给定的连接。

要创建租户连接,请设置CreateTenantConnection监听器:

// app/Providers/TenancyServiceProvider.php

Events\TenancyInitialized::class => [
    Listeners\CreateTenantConnection::class,
],

一般情况下,不建议使用此方法,因为您将失去自动模式的所有好处。但是,如果出于某种原因,对于您的项目而言使用这种方法更合理,您可以放心使用,因为该软件包不会出现任何问题。您可能无法像使用自动模式那样轻松地集成其他软件包,但是如果您的项目更适合使用这种方法,可以自由选择。

单数据库多租户

单数据库多租户在开发操作复杂性较低但代码复杂性较高,因为您需要手动对范围进行限定,无法集成某些第三方软件包。

当您在租户之间有太多共享资源,并且不想进行太多跨数据库查询时,这种方式更可取。

要使用单数据库多租户,请确保禁用负责为租户切换数据库连接的 DatabaseTenancyBootstrapper

您仍然可以使用其他 tenancy bootstrappers 来分离租户缓存、文件系统等。

还要确保已禁用数据库创建作业(CreateDatabaseMigrateDatabaseSeedDatabase …),不再监听 TenantCreated 事件。

概念

在单数据库多租户中,有四种类型的模型:

  • 租户模型(Tenant Model)

  • 主要模型(Primary Models) - 直接属于租户的模型

  • 次要模型(Secondary Models) - 间接属于租户的模型

    • 例如,评论(Comment)属于帖子(Post),帖子(Post)属于租户(Tenant)
    • 或者更复杂的情况下,投票(Vote)属于评论(Comment),评论(Comment)属于帖子(Post),帖子(Post)属于租户(Tenant)
  • 全局模型(Global Models) - 不受任何租户限制的模型

为了正确地限定查询范围,请在主要模型上应用 Stancl\Tenancy\Database\Concerns\BelongsToTenant trait。这将确保对父模型的所有调用都限定在当前租户范围内,并且对其子关联的调用通过父关系进行限定。

就这样,您的模型现在会自动限定在当前租户范围内,在没有当前租户的情况下(例如在中央管理面板中)不会进行任何范围限定。

但是,请牢记一个特殊情况。考虑以下设置:

class Post extends Model
{
    use BelongsToTenant;

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

class Comment extends Model
{
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

看起来是正确的,但您仍然可能会意外访问到其他租户的评论。

如果您使用以下代码:

Comment::all();

那么模型无法知道如何对该查询进行范围限定,因为它不直接属于该租户。实际情况是,您真的不应该经常这样做。您应该始终通过父模型访问次要模型。

然而,有时您可能有一个确实需要在租户上下文中这样做的用例。出于这个原因,我们还提供了 BelongsToPrimaryModel trait,它可以让您将上述调用范围限定为当前租户。它会在这些模型上自动加载父关系,并将其限定为当前租户。

因此,举个例子,您可以这样做:

class Comment extends Model
{
    use BelongsToPrimaryModel;

    public function getRelationshipToPrimaryModel(): string
    {
        return 'post';
    }

    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

这将自动将 Comment::all() 调用限定为当前租户。请注意,这种方法的局限性在于,您需要能够定义与主要模型的关系。因此,如果您需要在 “Vote” 中执行此操作(在 Vote属于Comment属于Post属于Tenant的情况下),您需要定义一些奇怪的关系。Laravel 支持 HasOneThrough,但不支持 BelongsToThrough,因此您需要围绕此进行一些调整。因此,我建议尽量避免使用这种类似 Comment::all() 的查询。

数据库考虑事项

唯一索引

如果您有一个唯一索引,例如:

$table->unique('slug');

在标准的非租户或多数据库租户应用程序中,您需要将此唯一索引限定到租户,这意味着您需要在主要模型上进行设置:

$table->unique(['tenant_id', 'slug']);

次要模型上进行设置:

// Imagine we're in a 'comments' table migration
$table->unique(['post_id', 'user_id']);

验证

uniqueexists 验证规则当然不会限定到当前租户,因此您需要手动进行限定,例如:

Rule::unique('posts', 'slug')->where('tenant_id', tenant('id'));

如果感觉很麻烦,您可以在自定义 Tenant 模型上使用 Stancl\Tenancy\Database\Concerns\HasScopedValidationRules trait,在该模型上添加这两个规则的方法。

您可以使用以下两个方法:

// 您可以使用 tenant() 助手函数检索当前租户。
// $tenant = tenant();

$rules = [
    'id' => $tenant->exists('posts'),
    'slug' => $tenant->unique('posts'),
]

低级数据库查询

还要记住的最后一件事是,DB 门面调用或任何其他类型的直接数据库查询当然不会限定到当前租户。

该软件包仅对 Eloquent 的抽象逻辑提供范围限定逻辑,对于低级数据库查询无能为力。

在使用它们时要小心。

进行全局查询

要禁用租户范围,请在查询中添加 withoutTenancy()

自定义列名

如果您想要自定义列名,例如使用 team_id 而不是 tenant_id —— 如果根据您的业务术语更合理 —— 您可以在服务提供程序或某个类中设置以下静态属性:

use Stancl\Tenancy\Database\Concerns\BelongsToTenant;

BelongsToTenant::$tenantIdColumn = 'team_id';

请注意,这适用于所有主要模型,如果您在某处使用了 team_id,则必须在所有地方使用它,无法同时使用 team_idtenant_id

租户识别

该软件包允许您使用以下方法识别租户:

  • 域名识别(acme.com
  • 子域名识别(acme.yoursaas.com
  • 域名或子域名识别(上述两者都可以)
  • 路径识别(yoursaas.com/acme/dashboard
  • 请求数据识别(yoursaas.com/users?tenant_id=acme - 或使用请求头)

然而,您可以自由地编写其他租户解析器

上述所有识别方法都附带有自己的中间件。您可以在下面了解有关每种识别方法的更多信息。

域名识别

要使用此识别方法,请确保您的租户模型使用 HasDomains trait。

请务必阅读文档中的Domains页面。

关系是 租户有多个域名。将主机名存储在 domains 表的 domain 列中。

这种识别方法附带 Stancl\Tenancy\Middleware\InitializeTenancyByDomain 中间件。

子域名识别

这与域名识别完全相同,只是将子域名存储在 domains 表的 domain 列中。

相较于将子域名的完整主机名存储在 domain 列中,这种方法的好处在于您可以在任何一个中央域上使用此子域名。

这种方法的中间件是 Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain

域名/子域名组合识别

如果您希望同时使用子域名和域名,请使用 Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain 中间件。

domain 列中包含的记录将被视为域名/主机名(例如 foo.bar.com),而不包含任何点的记录将被视为子域名(例如 foo)。

路径识别

有些应用程序希望使用单个域名,但使用路径来标识租户。这是当您希望客户使用您的品牌产品而不是给他们一个可以在自己的域上使用的白标产品时。

要实现此目的,请使用 Stancl\Tenancy\Middleware\InitializeTenancyByPath 中间件并确保您的路由以 /{tenant} 为前缀

use Stancl\Tenancy\Middleware\InitializeTenancyByPath;

Route::group([
    'prefix' => '/{tenant}',
    'middleware' => [InitializeTenancyByPath::class],
], function () {
    Route::get('/foo', 'FooController@index');
});

如果您想自定义参数的名称(例如,使用 team 而不是 tenant),请查看 PathTenantResolver 的公共静态属性。

请求数据识别

您可能希望基于请求数据(请求头或查询参数)识别租户。具有 SPA 前端和 API 后端的应用程序可能希望使用此方法。

此识别方法的中间件是 Stancl\Tenancy\Middleware\InitializeTenancyByRequestData

您可以自定义此中间件在请求中查找什么。默认情况下,它会查找 X-Tenant 请求头。如果找不到该头,则会查找 tenant 查询参数。

如果您想使用其他请求头,请更改静态属性:

use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;

InitializeTenancyByRequestData::$header = 'X-Team';

如果您只想使用查询参数识别,将头静态属性设置为 null

use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;

InitializeTenancyByRequestData::$header = null;

如果您想禁用查询参数识别,并始终只使用头,则将参数的静态属性设置为 null

use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;

InitializeTenancyByRequestData::$queryParameter = null;

手动识别租户

请参阅手动初始化页面,了解如何手动识别租户。

自定义 onFail 逻辑

每个识别中间件都有一个静态 $onFail 属性,可用于自定义无法识别租户时的行为。

\Stancl\Tenancy\Middleware\InitializeTenancyByDomain::$onFail = function ($exception, $request, $next) {
    return redirect('https://my-central-domain.com/');
};

提前识别

使用基于路由中间件的自动方法来根据路由切换应用程序的上下文有一个小“陷阱”,那就是路由级别的中间件在控制器构造函数之后执行

这意味着如果您使用依赖注入来在控制器构造函数中注入一些服务,它们将从中央上下文中读取,因为路由级别的中间件尚未初始化租户。

有两种解决方法,前者更可取。

不使用构造函数依赖注入

您可以在路由的动作中注入依赖项,即:如果您有一个绑定了 Post 模型的路由,您仍然可以像这样注入依赖项:

// 注意这是伪代码。注意路由动作的 DI,忽略其余部分 :)

Route::get('/post/{post}/edit', 'PostController@edit');

class PostController
{
    public function edit(Request $request, Post $post, Cloudinary $cloudinary)
    {
        if ($request->has('image')) {
            $post->image_url = $cloudinary->store($request->file('image'));
        }
    }
}

如果您不喜欢这种方式,因为您需要从许多动作访问某些依赖项,请考虑创建一个记忆化方法:

class PostController
{
    protected Cloudinary $cloudinary;

    protected cloudinary(): Cloudinary
    {
        // 如果您不喜欢这里的服务定位,可以在控制器构造函数中注入 Application 是一个 100% 安全的方法。
        return $this->cloudinary ??= app(Cloudinary::class);
    }

    public function edit(Request $request, Post $post)
    {
        if ($request->has('image')) {
            $post->image_url = $this->cloudinary()->store(
                $request->file('image')
            );
        }
    }
}

使用更复杂的中间件设置

注意:v3 中有一个新的中间件用于阻止从中央域访问。v2 的做法有些不同。

关于如何实现这一点的手册即将推出,现在您可以查看 v2.x 是如何实现的。

简而言之:InitializeTenancy 中间件是全局中间件栈的一部分,它无法访问路由信息,但在控制器构造函数之前执行。PreventAccessFromTenantDomains 中间件检查我们是否在租户域上访问租户路由或在中央域上访问中央路由,如果不是,则终止请求,可以通过 404 或将我们重定向到租户域上的主页 URL。

以下是逻辑的可视化表示:

提前识别中间件

以下是 v2.x 代码库中相关的文件:

多数据库租户

该软件包提供了处理多数据库租户所需的所有工具。

TenantDatabaseManagers

TenantDatabaseManagers 是管理租户数据库的类,它们主要负责创建和删除这些数据库。

对于所有 Laravel 支持的数据库驱动程序(MySQL、PostgreSQL、SQLite),都有相应的数据库管理器。此外,还有一个适用于使用单个数据库但具有多个模式(每个租户一个模式)的 PostgreSQL 的数据库管理器。

有关更多详细信息,请参阅租户配置文件中的 database 部分。

命令

还有一些用于处理租户数据库的命令,即 tenants:migratetenants:seed。请参阅文档的控制台命令页面

作业和监听器

默认情况下,当创建一个租户时,还会为该租户创建一个数据库。这是通过 TenancyServiceProvider 中的 JobPipeline 监听器完成的。请参阅文档的事件系统页面

迁移

将您的租户迁移文件移动到 database/migrations/tenant 目录中。您可以使用 php artisan tenants:migrate 命令来执行它们。

请注意,所有迁移文件都共享相同的 PHP 命名空间,因此即使在中央数据库和租户数据库中使用相同的表名,您仍然需要使用不同的迁移(类)名称。

如果您在项目开发中采用模块化方法,可能会在多个位置上拥有租户迁移文件。幸运的是,您可以指定软件包应该在哪里查找租户迁移文件。database/migrations/tenant 目录只是默认设置。

要设置这些路径,请打开 config/tenancy.php 文件,并更改 migration_parameters 中的 <code>--paths</code> 参数的值。您可以在该数组中指定任意数量的路径。

自定义数据库

您可以通过在租户上存储特定的内部键来自定义租户的数据库连接方式。

如果您更改了租户模型上的内部前缀,则应使用该前缀,而不是 tenancy_

指定数据库名称

您可以在创建租户时设置 tenancy_db_name 键来指定租户的数据库名称。

Tenant::create([
    'tenancy_db_name' => 'acme',
]);

当您没有指定租户的数据库名称时,它将使用以下方式构建:

tenancy.database.prefix 配置 + 租户 ID + tenancy.database.suffix 配置

因此,另一种指定数据库名称的方式是在创建租户时设置租户 ID,而不是让其随机生成:

Tenant::create([
    'id' => 'acme',
]);

指定数据库凭据

仅当您使用权限控制的 MySQL 数据库管理器时,才会创建数据库用户名和密码。有关更多信息,请参阅数据库配置。

您可以为将与租户数据库一起创建的用户指定用户名和密码。

Tenant::create([
    'tenancy_db_username' => 'foo',
    'tenancy_db_password' => 'bar',
]);

用户将获得在 PermissionControlledMySQLDatabaseManager::$grants 数组中指定的授权。您可以根据需要自定义此属性,将其设置为其他值,就像设置其他公共静态属性一样。

请注意,您不希望授予用户授予自己更多授权的能力。

指定模板连接

重要:config/database.php 中不应该有 tenant 连接。如果为租户创建了一个模板连接,请将其命名为 tenant_template 或其他类似的名称。tenant 连接完全由该软件包管理,并在结束租期时重置为 null

要指定用于构建此租户的数据库连接(类似于 config/database.php 中的数组),请设置 tenancy_db_connection 键。否则,将使用 tenancy.database.template_connection 配置中的连接名称。如果该键为 null,则将使用中央连接。

指定其他连接细节

您还可以设置特定的连接细节,而不必创建一个新连接。最终的 “连接数组” 将通过合并以下内容构建:

  • 模板连接
  • 数据库名称
  • 可选的用户名和密码
  • 所有 tenancy_db_*

这意味着您可以存储某个值,例如 tenancy_db_charset,以指定租户数据库连接的字符集,无论出于何种原因。

租户之间的同步资源

如果您想在租户数据库之间共享某些资源(通常是用户),您可以使用我们的资源同步功能。这将允许您在特定租户数据库和中央数据库之间同步特定列。

这是一个相对复杂的功能,因此在实施之前,请确保您确实需要它。只有在使用多数据库租户并且需要在不同租户的数据库之间同步特定资源(如用户)时,您才需要此功能。

数据库

资源存在于中央数据库中,例如一个 users 表。在租户的数据库中存在另一个表。它可以使用与中央数据库相同的名称或不同的名称 - 由您决定。

然后,在中央数据库中有一个枢纽表,将资源(在我们的例子中是 users)与租户相关联。

资源不会与所有租户数据库同步 - 这是不希望的,例如,用户通常只存在于少数几个租户中。

概念

您将需要两个模型来表示资源。一个用于租户数据库,另一个用于中央数据库。租户模型必须实现 Syncable 接口,而中央模型必须实现 SyncMaster 接口。

SyncMasterSyncable 的扩展,它需要一个额外的方法 - 与租户的关联,以知道哪些租户也拥有此资源。

这两个模型都必须使用 ResourceSyncing trait。此 trait 确保每当模型保存时,会触发一个 SyncedResourceSaved 事件。UpdateSyncedResource 监听器将更新中央数据库中的资源以及资源存在的所有租户数据库。此监听器在 TenancyServiceProvider 中注册。

请注意,当由于同步而更新或创建模型时,该模型将使用 withoutEvents 方法调用,并且如果您依赖于保存或创建事件,则需要以其他方式实现它。

Syncable 接口的一个重要要求是 getSyncedAttributeNames() 方法。您不希望同步所有列(或更具体地说,属性,因为我们正在讨论 Eloquent 模型 - 支持访问器和修改器)。在 users 示例中,您可能只想同步像姓名、电子邮件和密码这样的属性,同时保留特定于租户的属性(或者特定于工作区/团队,根据您项目的术语)的独立性。

资源在中央数据库和租户数据库中需要具有相同的全局 ID。

工作原理

让我们来写个例子实现:

use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Database\Models\TenantPivot;

class Tenant extends BaseTenant implements TenantWithDatabase
{
    use HasDatabase, HasDomains;

    public function users()
    {
        return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id')
            ->using(TenantPivot::class);
    }
}

class CentralUser extends Model implements SyncMaster
{
    // 注意,我们在这个模型上强制使用中央连接
    use ResourceSyncing, CentralConnection;

    protected $guarded = [];
    public $timestamps = false;
    public $table = 'users';

    public function tenants(): BelongsToMany
    {
        return $this->belongsToMany(Tenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id')
            ->using(TenantPivot::class);
    }

    public function getTenantModelName(): string
    {
        return User::class;
    }

    public function getGlobalIdentifierKey()
    {
        return $this->getAttribute($this->getGlobalIdentifierKeyName());
    }

    public function getGlobalIdentifierKeyName(): string
    {
        return 'global_id';
    }

    public function getCentralModelName(): string
    {
        return static::class;
    }

    public function getSyncedAttributeNames(): array
    {
        return [
            'name',
            'password',
            'email',
        ];
    }
}

class User extends Model implements Syncable
{
    use ResourceSyncing;

    protected $guarded = [];
    public $timestamps = false;

    public function getGlobalIdentifierKey()
    {
        return $this->getAttribute($this->getGlobalIdentifierKeyName());
    }

    public function getGlobalIdentifierKeyName(): string
    {
        return 'global_id';
    }

    public function getCentralModelName(): string
    {
        return CentralUser::class;
    }

    public function getSyncedAttributeNames(): array
    {
        return [
            'name',
            'password',
            'email',
        ];
    }
}

// Pivot 表迁移
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTenantUsersTable extends Migration
{
    public function up()
    {
        Schema::create('tenant_users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('tenant_id');
            $table->string('global_user_id');

            $table->unique(['tenant_id', 'global_user_id']);

            $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
            $table->foreign('global_user_id')->references('global_id')->on('users')->onUpdate('cascade')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('tenant_users');
    }
}

以下是它的工作原理:

  • 您在中央数据库中创建一个用户。它仅存在于中央数据库中。
$user = CentralUser::create([
    'global_id' => 'acme',
    'name' => 'John Doe',
    'email' => 'john@localhost',
    'password' => 'secret',
    'role' => 'superadmin', // 未同步
]);
  • 现在,您在租户的数据库中创建一个具有相同全局 id 的用户:
tenancy()->initialize($tenant);

// 在租户数据库中创建相同的用户
$user = User::create([
    'global_id' => 'acme',
    'name' => 'John Doe',
    'email' => 'john@localhost',
    'password' => 'secret',
    'role' => 'commenter', // 未同步
]);
  • 您在租户上更新一些属性:
$user->update([
    'name' => 'John Foo', // 已同步
    'email' => 'john@foreignhost', // 已同步
    'role' => 'admin', // 未同步
]);
  • 中央用户的 nameemail 已更改。

如果您创建更多的租户并在这些租户的数据库中创建用户,则更改将在所有这些租户的数据库和中央数据库之间同步。

在租户的数据库中创建用户将 1:1 地将资源复制到中央数据库,包括未同步的列(这里它们作为默认值)。

将资源附加到租户

您可以看到在上面的示例中,我们使用 TenantPivot 模型来表示 BelongsToMany 关系。这使我们能够从中央数据库级联同步资源到租户:

$user = CentralUser::create(...);

$user->tenants()->attach($tenant);

将租户附加到用户将复制甚至未同步的列(它们作为默认值),类似于在租户的数据库中创建用户将 1:1 地将租户复制到中央数据库。

如果您想使用自定义枢纽模型,请查看 TenantPivot 的源代码,以了解要复制的内容(或扩展它),如果要保留此行为。

还要注意,如果您在租户的数据库中创建一个用户,全局 id 将使用 ID 生成器创建。如果您禁用了用于增量租户 id 的 ID 生成器,请进行一些更改。

队列

在生产环境中,您几乎肯定希望将将更改复制到其他数据库的监听器加入到队列中。要做到这一点,请更改监听器的静态属性:

\Stancl\Tenancy\Listeners\UpdateSyncedResource::$shouldQueue = true;

会话范围

会话范围是您可能需要自行处理的事项之一。

当您使用多个租户域名和数据库时,用户可以更改其会话 cookie 的域,其会话数据将在另一个租户的应用程序中共享。

以下是您可以解决此问题的方法。

将会话存储在数据库中

由于数据库会自动分隔,因此只需将数据库用作会话驱动程序即可完全消除此问题。

将会话存储在 Redis 中

这与使用 DB 会话驱动程序的解决方案相同。如果您使用 RedisTenancyBootstrapper,您的 Redis 数据库将自动为您的租户分隔,并且因此,存储在这些 Redis 数据库中的任何会话将被正确地限定范围。

使用中间件防止会话伪造

或者,您可以在租户路由上使用 Stancl\Tenancy\Middleware\ScopeSessions 中间件,以确保任何试图操纵会话的尝试都会导致 403 未授权响应。

这将适用于所有存储驱动程序,但前提是您使用每个租户一个域的方式。如果您使用路径识别,则需要在数据库中存储会话(如果使用多数据库租户),或者需要使用单数据库租户(这在路径识别中可能更常见)。

队列

如果您使用 QueueTenancyBootstrapper,从租户上下文中调度的排队作业将自动具备租户感知能力。作业将集中存储 - 如果您使用数据库队列驱动程序,它们将存储在中央数据库的 jobs 表中。在处理作业之前,当前租户的租期将被初始化。

然而,请注意,如果您使用 DatabaseTenancyBootstrapperdatabase 队列驱动程序,或者使用 RedisTenancyBootstrapperredis 队列驱动程序,您需要确保不将作业调度到这些驱动程序的租户上下文中。

数据库队列驱动程序

为了强制数据库队列驱动程序使用中央连接,请打开您的 queue.connections.database 配置文件,并添加以下行:

'connection' => 'central',

(将 central 替换为您中央数据库连接的名称。)

Redis 队列驱动程序

确保队列使用的连接不在 tenancy.redis.prefixed_connections 中。

中央队列

从中央上下文调度的作业将始终保持为中央作业。然而,建议不要在中央和租户作业之间混合使用队列 连接,以避免潜在的全局状态残留,例如中央作业认为它们处于先前租户的上下文中。

要以任何情况下都在中央运行作业,请创建一个新的队列连接,并将 central 键设置为 true。例如:

// queue.connections
'central' => [
    'driver' => 'database',
    'table' => 'jobs',
    'queue' => 'default',
    'retry_after' => 90,
    'central' => true, // <---
],

并像这样使用此连接:

dispatch(new SomeJob(...))->onConnection('central');

测试

如果您是赞助商,您可以在网站上获取一个有主观但自动化的测试设置,其中包含专属内容供赞助商使用:sponsors.tenancyforlaravel.com/fri...

TODO:Review

事件

请记住,此包在事件方面使用非常频繁,所以如果您在测试中的任何地方使用了 Event::fake(),则租户初始化和相关进程可能会中断。

因此,请尽量选择性地模拟测试。例如,使用:

Event::fake([MyEvent::class]);

而不是:

Event::fake();

中央应用

要测试您的中央应用程序,只需编写普通的 Laravel 测试即可。

租户应用

注意:如果您使用多数据库租户和自动模式,则无法使用 :memory: 的 SQLite 数据库或 RefreshDatabase 特性,因为默认数据库会切换。

要测试应用程序的租户部分,请在 setUp() 方法中创建一个租户并初始化租赁。

您可能还想这样做:

class TestCase // extends ...
{
    protected $tenancy = false;

    public function setUp(): void
    {
        parent::setUp();

        if ($this->tenancy) {
            $this->initializeTenancy();
        }
    }

    public function initializeTenancy()
    {
        $tenant = Tenant::create();

        tenancy()->initialize($tenant);
    }

    // ...
}

然后在您的各个测试用例中:

class FooTest extends TestCase
{
    protected $tenancy = true;

    /** @test */
    public function some_test()
    {
        $this->assertTrue(...);
    }
}

或者您可能希望为租户测试创建一个单独的 TestCase 类,以获得更好的组织。

与其他包集成

如果您正在使用自动模式和多数据库租户,您可以轻松地与其他包集成。

该包提供了一些有用的 artisan 命令。

默认情况下,租户感知命令将为所有租户运行。命令还有一个 --tenants 选项,可以指定要运行命令的租户的ID。

注意:要在CLI中包含多个租户,可以使用多个 --tenants=<...> 选项。如果使用 Artisan::call() 调用命令,则 --tenants 必须是一个数组。

迁移 (租户感知)

tenants:migrate 命令会迁移您的租户的数据库。

php artisan tenants:migrate --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23

注意:默认情况下,迁移应该位于 database/migrations/tenant 目录中。如果您希望使用不同的路径,可以使用 --path 选项。您还可以在租租户配置中指定命令的默认参数。

回滚和填充 (租户感知)

  • 回滚:tenants:rollback
  • 填充:tenants:seed

注意:您可以在租户配置中配置 tenants:seed 的默认参数(例如使用自定义的租户填充器)。

新迁移 (租户感知)

该包还提供了一个简化的、租户感知版本的 migrate:fresh 命令。它会在租户的数据库上运行 db:wipetenants:migrate

您可以像这样使用它:

php artisan tenants:migrate-fresh --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23

运行 (租户感知)

您可以使用 tenants:run 命令为租户运行自己的命令。

如果您的命令签名是 email:send {--queue} {--subject=} {body},那么您可以像这样运行该命令:

php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..."

或者使用 Artisan::call()

Artisan::call('tenants:run', [
    'commandname' => 'email:send', // String
    '--tenants' => ['8075a580-1cb8-11e9-8822-49c5d8f8ff23'], // Array
    '--option' => ['queue=1', 'subject=New Feature'], // Array
    '--argument' => ['body=We have launched a new feature.'], // Array
]);

列出

tenants:list 命令列出所有现有的租户。

php artisan tenants:list

选择性地清除租户缓存

您可以通过在 cache:clear 命令上使用 --tags 选项来删除特定租户的缓存:

php artisan cache:clear --tags=tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339

标签由 config('tenancy.cache.tag_base') . $id 派生。

租户感知命令

即使 tenants:run 命令可以让您为租户运行任意 artisan 命令,但您可能希望有严格的租户命令。

要使命令有租户感知功能,请使用 TenantAwareCommand trait:

class MyCommand extends Command
{
    use TenantAwareCommand;
}

然而,该 trait 要求您实现一个返回 Tenant 实例数组的 getTenants() 方法。

如果您不想自己实现选项/参数,可以使用以下两个 trait 之一:

  • HasATenantsOption - 接受多个租户ID,可选的 – 默认情况下,该命令将为所有租户执行
  • HasATenantArgument - 接受单个租户ID,必需的参数

这些 trait 实现了 TenantAwareCommand 所需的 getTenants() 方法。

注意:如果您在命令中使用自定义构造函数,则需要在结尾处添加 $this->specifyParameters(),以便选项/参数 trait 生效。

因此,如果您将这些 trait 与 TenantAwareCommand 结合使用,您不需要更改命令中的任何内容:

class FooCommand extends Command
{
    use TenantAwareCommand, HasATenantsOption;

    public function handle()
    {
        // 
    }
}

class BarCommand extends Command
{
    use TenantAwareCommand, HasATenantArgument;

    public function handle()
    {
        // 
    }
}

自定义实现

如果您想要更多控制权,可以自己实现此功能,只需接受一个 tenant_id 参数,然后在 handle() 方法中执行以下操作:

tenancy()->find($this->argument('tenant_id'))->run(function () {
    // 您的实际命令代码
});

租户属性加密

要加密租户模型上的属性,请将其存储在自定义列中,并将属性转换为 'encrypted' 或您自定义的加密转换。

例如,我们将加密租户的数据库凭据 - tenancy_db_usernametenancy_db_password。我们需要为这些属性创建自定义列,因为默认情况下,它们存储在虚拟的 data 列中。

  1. 在租户表中添加自定义列(我们建议将字符串大小设置为至少512个字符,以便字符串能够容纳加密数据):
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTenantsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return  void
     */
    public function up(): void
    {
        Schema::create('tenants', function (Blueprint $table) {
            $table->string('id')->primary();

            // Your custom columns
            $table->string('tenancy_db_username', 512);
            $table->string('tenancy_db_password', 512);

            $table->timestamps();
            $table->json('data')->nullable();
        });
    }
}
  1. 在租户模型上定义自定义列:
public static function getCustomColumns(): array
{
    return [
        'id',
        'tenancy_db_username',
        'tenancy_db_password',
    ];
}
  1. 然后在模型上为属性定义转换(使用 Laravel 的加密转换或您自定义的转换):
protected $casts = [
    'tenancy_db_username' => 'encrypted',
    'tenancy_db_password' => 'encrypted',
];

缓存查找

如果您正在使用多个数据库,您可能希望避免在每个租户请求中对中央数据库进行查询以进行租户标识。尽管查询非常简单,但应用程序必须与中央数据库建立连接,这是昂贵的。

为了避免这种情况,您可以在租户解析器上启用缓存(全部位于Stancl\Tenancy\Resolvers命名空间中):

  • DomainTenantResolver(也用于子域名标识)
  • PathTenantResolver
  • RequestDataTenantResolver

在每个这些类上,您可以设置以下静态属性:

// TenancyServiceProvider::boot()

use Stancl\Tenancy\Resolvers;

// enable cache
DomainTenantResolver::$shouldCache = true;

// seconds, 3600 is the default value
DomainTenantResolver::$cacheTTL = 3600;

// specify some cache store
// null resolves to the default cache store
DomainTenantResolver::$cacheStore = 'redis';

缓存失效

当将 DomainTenantResolver::$shouldCache 设置为 true 时,更新和保存租户模型的属性将导致缓存中的该模型条目失效。

您可以通过调用以下方法来使缓存失效:

app(\Stancl\Tenancy\Resolvers\DomainTenantResolver::class)->invalidateCache($tenant);

注意:当使用域名标识时,缓存的键包含域名的名称。如果您打算更新域名,请在进行任何更改之前使缓存失效。

实时门面

当使用storage_path()后缀(用于本地文件系统租户)时,每个租户都会在storage/中获得一个单独的子目录。

这意味着存储路径看起来像这样:

storage/tenant123/app/foo.png

而不是像这样:

storage/app/tenant123/foo.png

这意味着storage/中的其他目录也是租户范围的,特别是framework目录。

实时门面的问题

当使用实时门面时,Laravel会创建一个带有门面代码的PHP文件,并将其存储在storage_path/framework/cache中,并自动加载它。

为租户创建框架目录

为了解决这个问题,您需要为租户创建这些目录。但请注意,只有在以下情况下才需要这样做:

  1. 您正在使用storage_path()后缀(在tenancy配置中启用)。
  2. 您正在使用实时门面。

您可以使用事件系统来创建这些目录。

使用作业流水线,因为您需要初始化租户以运行此代码,并且您需要可能初始化租户(例如,在创建租户的数据库之前,您无法初始化租户)。

将以下作业添加到您的TenantCreated作业流水线中:

<?php

namespace App\Jobs;

use Stancl\Tenancy\Contracts\Tenant;

class CreateFrameworkDirectoriesForTenant
{
    protected $tenant;

    public function __construct(Tenant $tenant)
    {
        $this->tenant = $tenant;
    }

    public function handle()
    {
        $this->tenant->run(function ($tenant) {
            $storage_path = storage_path();

            mkdir("$storage_path/framework/cache", 0777, true);
        });
    }
}

租户维护模式

您可以使用MaintenanceMode特性将特定的租户置于维护模式。

在您的Tenant model上应用它:

use Stancl\Tenancy\Database\Concerns\MaintenanceMode;

class Tenant extends BaseTenant
{
    use MaintenanceMode;
}

这样,您可以在每个租户对象上使用以下方法:

$tenant->putDownForMaintenance();

要将特定租户从维护模式中移除:

$tenant->update(['maintenance_mode' => null]);

中间件

您还需要在租户路由上使用Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode中间件。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 22
mouyong

doc.wyz.xyz/pages/30ee05/ 这个不是吗?

可以试试这个 github.com/plugins-world/laravelsa... ,30分钟内完成 laravel 项目的 saas 化。

1年前 评论
Huan_09 (楼主) 1年前
mouyong (作者) 1年前
Huan_09 (楼主) 1年前
mouyong (作者) 1年前
Huan_09 (楼主) 1年前
随波逐流

这个扩展有坑,慎用。

用这个扩展 队列批处理JOB 会有丢失数据库链接的问题。处理普通job时,不能使用模型传参,解决方案是传递非模型的 int/string 数据类型。在handel 里面调用 tenancy()->initialize(tenantId) 初始化租户,在进行业务处理稳妥。

1年前 评论
mouyong 1年前
mouyong 1年前
随波逐流 (作者) 1年前
随波逐流 (作者) 1年前
91it 1年前
mouyong 1年前
随波逐流 (作者) 1年前
随波逐流 (作者) 1年前
mouyong 1年前
老五 7个月前
DogLoML

mark一下

1年前 评论
抄你码科技有限公司

file

@Huan_09 大佬,感觉这一整篇好长呀,不便于阅读,不知道你的号能不能创建文档?

1年前 评论
Huan_09 (楼主) 1年前

求问各位大佬,就是saas关键的怎么给每个租户分配应用和功能权限的文档在哪里呀,怎么每个文档里面都没看到呢,是扩展里面没有,要自己写逻辑吗?是用SyncMaster 和 Syncable接口吗,这两个是什么意思啊

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!