Laravel Tenancy 中文文档 (自用)

引言

Tenancy被开发成一个小型生态系统。它是一组产品/包,都专注于一个简单的目标:为您的应用程序提供最佳的多租户工作方式。

这个想法

Tenancy的一般思路是以一种非常强大的模块化方式在任何Laravel应用程序中实现多租户工作。

通过使用模块化方法,我们允许您仅选择您特定应用程序所需的组件。这使您能够只实现您需要的特定功能,而不必强制实现额外的功能来改变应用程序的工作方式。

要求

知识

多租户在理解上可能非常困难,更不用说在应用程序中应用了。这就是为什么我们建议每个人在开始使用Tenancy之前先熟悉Laravel应用程序开发。

软件

Tenancy始终希望为其包支持最新的Laravel版本。这个版本的Tenancy支持:

  • Laravel 9.x
  • PHP 8.0或更高版本

由于文档有时可能过时,我们强烈建议您查看最新的Github Actions

模块特定要求

根据使用的Tenancy模块的不同,可能会有其他特定于模块的要求。请确保检查您安装的每个模块以确保满足要求。

安装

基本安装

有两种方法可以安装基本包,即简化安装和选择性安装。

简化安装方法将安装基本包和所有其他模块,但不会注册所有提供者。

选择性安装方法只安装基本包,每个附加模块都需要单独安装。

我们强烈建议使用选择性安装方法,因为它确保只安装您需要的模块,并自动注册相应的提供者。

简化安装

要快速进行简化安装,可以一次性安装所有内容:

composer require tenancy/tenancy

重要提示: 您需要手动启用您选择的数据库驱动,方法是将相关的服务提供者添加到 app.php 文件。

注意: 这将默认加载特定的非侵入式影响。您可以通过查看存储库的 composer.json 文件来检查加载了哪些影响。

这是一种轻松试用这个工具包的方法!

我们建议有选择性地安装您需要的包。安装了 tenancy/tenancy 之后,确保注册您希望使用的影响和钩子。您只需简单地注册相应的提供者即可。

以下是可以添加到 config/app.php 文件中的非详尽提供者列表,请确保注释掉您不需要的部分。每个组件的文档中都包含所需的确切提供者。

'providers' => [
    // ...

    /*
     * Package Service Providers...
     */
    Tenancy\Affects\Broadcasts\Provider::class,
    Tenancy\Affects\Cache\Provider::class,
    Tenancy\Affects\Configs\Provider::class,
    Tenancy\Affects\Connections\Provider::class,
    Tenancy\Affects\Filesystems\Provider::class,
    Tenancy\Affects\Logs\Provider::class,
    Tenancy\Affects\Mails\Provider::class,
    Tenancy\Affects\Models\Provider::class,
    Tenancy\Affects\Routes\Provider::class,
    Tenancy\Affects\URLs\Provider::class,
    Tenancy\Affects\Views\Provider::class,

    Tenancy\Hooks\Database\Provider::class,
    Tenancy\Hooks\Migration\Provider::class,
    Tenancy\Hooks\Hostname\Provider::class,

    Tenancy\Database\Drivers\Mysql\Provider::class,
    Tenancy\Database\Drivers\Sqlite\Provider::class,
    // ...
]

选择性安装

不要一次性加载所有 tenancy 包,您可以选择性地安装您需要的内容。您至少需要安装框架:

composer require tenancy/framework

下一步

现在安装了基本包,您需要确定您的租户对象是什么,并对租户对象执行设置。

之后,选择性地添加和/或配置以下内容:

Tenancy 的架构分为三个主要组件类型。

生命周期钩子

这些组件关注的是在系统中创建、更新或删除租户时发生的事情。这些组件涉及的内容包括确保租户注册的域名有效,创建数据库,并注册/创建处理租户需求的服务。

了解更多

标识

Tenancy 的标识组件负责在应用程序逻辑发生之前识别租户,无论该逻辑是来自 HTTP 请求、队列中运行的作业还是在控制台中运行的命令。

了解更多

影响

影响组件负责为已识别的租户更改应用程序。这些组件包括设置与数据库的连接、更改日志设置和其他服务的配置等操作。

了解更多

什么是租户

概述

无论是基于请求的主机名、登录的用户还是用户所属的团队来分离数据,完全取决于您自己。租户的主题可以是应用程序中的任何 eloquent 模型,您希望围绕其构建业务逻辑。

选择租户

在构建多租户应用程序时,最重要的决策是选择租户。为了帮助您做出决策,请自问以下问题:

  • 我的业务逻辑中的租户数据属于哪个实体?
  • 我是否有公司与员工注册?那么很可能该公司是租户,它保存了我们希望与应用程序中的任何其他公司分隔开的所有信息。这在使用自定义 URL 的应用程序中是一个非常常见的选择,例如 yourteam.saas.app。
  • 或者,我们可以简单地将用户标记为租户。这将将所有数据连接到单个帐户。Gmail 就是一个很好的例子。您登录后只能看到您帐户的电子邮件。
  • 还有一种混合版本也很常见,GitHub 提供个人命名空间,如 github.com/luceos 和团队命名空间,如 github.com/tenancy。

Tenancy 对于任何情况都提供解决方案,因为对租户的标识没有限制,也没有配置的租户数量限制。以上述混合示例为例,租户既可以是用户模型,也可以是组织模型。通过应用契约并实现所需的方法,它们可以被标记为有效的租户实体。

下一步

一旦确定了将成为租户的对象,您将需要设置这些租户。要开始,请继续阅读租户设置

租户设置

租户契约

租户契约 Tenancy\Identification\Contracts\Tenant 将特定的类标记为有效的租户。让该类实现契约方法以开始使用:

use Illuminate\Database\Eloquent\Model;
use Tenancy\Identification\Contracts\Tenant;

class User extends Model implements Tenant
{
    /**
     * The attribute of the Model to use for the key.
     *
     * @return string
     */
    public function getTenantKeyName(): string
    {
        return 'id';
    }

    /**
     * The actual value of the key for the tenant Model.
     *
     * @return string|int
     */
    public function getTenantKey()
    {
        return $this->id;
    }

    /**
     * A unique identifier, eg class or table to distinguish this tenant Model.
     *
     * @return string
     */
    public function getTenantIdentifier(): string
    {
        return get_class($this);
    }
}

这将强制您的 User 模型实现一些 tenancy 所需的方法。

租户模型 Trait

当然,tenancy 提供了一种简单的方式,通过使用 trait 来为模型应用契约所需的方法。

use Illuminate\Database\Eloquent\Model;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;

class User extends Model implements Tenant
{
    use AllowsTenantIdentification;
}

您的第一个有效租户已经存在。现在我们需要确保我们的应用程序知道它的存在。

租户注册

为了让包知道一个有效的租户存在,您需要在租户识别解析器上注册它。最好的方法是在 app/Providers/AppServiceProvider 中或者是您创建的专用 app/Providers/TenantProvider 中进行注册。通过使用 addModel 函数将租户提供给租户解析器。

namespace App\Providers;

use App\User;
use Illuminate\Support\ServiceProvider;
use Tenancy\Identification\Contracts\ResolvesTenants;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->resolving(ResolvesTenants::class, function (ResolvesTenants $resolver) {
            $resolver->addModel(User::class);

            return $resolver;
        });
    }
}

下一步

生命周期钩子

生命周期钩子允许您在创建、更新或删除租户时执行特定操作。详细信息请查看生命周期钩子

生命周期钩子

生命周期钩子在创建、更新或删除租户时执行。它们允许对新创建的租户进行简单的引导,例如提供新数据库虚拟机、注册域名或设置 S3 存储桶。

HookResolver 触发钩子时,它将执行以下操作:

  • 解析已注册的钩子。
  • 调用钩子的 for() 方法,并传递特定的生命周期事件
  • 通过检查 fires() 方法的返回值来过滤不应触发的钩子。
  • 根据 priority() 方法的返回值进行排序,参见优先级
  • 直接调用 fire() 方法,或者当 queued() 为 true 时,将其派发到队列返回的任务。

事件

生命周期钩子监听以下任一事件:

  • Tenancy\Tenant\Events\Created
  • Tenancy\Tenant\Events\Updated
  • Tenancy\Tenant\Events\Deleted

您需要在自己的代码库中触发这些事件,例如:

event(new \Tenancy\Tenant\Events\Created($tenant));

或者通过 Laravel 的 $dispatchesEvents

use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    protected $dispatchesEvents = [
        'created' => \Tenancy\Tenant\Events\Created::class,
        'updated' => \Tenancy\Tenant\Events\Updated::class,
        'deleted' => \Tenancy\Tenant\Events\Deleted::class,
    ];
}

需要注意的一些要点:

  • 租户必须实现租户契约
  • 在您自己的代码完成之后触发事件。
  • 在任何事务之外触发事件,以便已更改的属性持久化,并且钩子使用正确的值。

优先级

要确保钩子按正确的顺序执行(例如,在创建数据库后运行迁移),钩子需要具有优先级。确保您的钩子使用正确的值。钩子按从最低优先级到最高优先级的顺序运行。

  • 创建、更新和删除数据库:-100
  • 如果启用了 hooks-migrations:-50

因此,如果您想在创建租户时设置和配置数据库服务器,请确保使用优先级低于 -100 的值。如果您想在迁移和数据填充完成后动态配置数据库值,请使用高于 -50 的值。

可用的钩子

内置钩子

下一步

标识

标识驱动将确定您的应用程序在请求、命令或作业期间应加载哪个租户。

生命周期钩子 - 数据库

概述

此包的目的是处理租户数据库的创建、更新和删除。

要使用此包,还必须安装数据库驱动

需要有一个租户,并分发生命周期事件中的 CreatedUpdated 和/或 Deleted 事件才能使用此包。

安装

使用 Composer 安装:

composer require tenancy/hooks-database

注册以下 ServiceProvider:

Tenancy\Hooks\Database\Provider::class

配置

建议监听 Tenancy\Hooks\Database\Events\Drivers\Configuring 事件以调整数据库配置。

可用的函数有:

  • useConnection():使用在 Laravel 应用程序的 config/database.php 文件中定义的连接之一。
  • useConfig():使用任意路径中的文件提供配置。
  • defaults():使用 Tenancy 内部代码来获取以下配置:数据库名称(租户键)、数据库用户名(租户键)、数据库用户密码(基于 tenancy/framework 中的 PasswordGenerator 生成)。

以下示例将配置数据库使用 config/database.php 中定义的 mysql 连接,并将 Tenancy 的默认数据库配置与此合并:

namespace App\Listeners;

use Tenancy\Hooks\Database\Events\Drivers\Configuring;

class ConfigureTenantDatabase
{
    public function handle(Configuring $event)
    {
        $event->useConnection('mysql', $event->defaults($event->tenant));
    }
}

数据库变异

Tenancy 提供了一个特定的事件 Tenancy\Hooks\Database\Events\ConfigureDatabaseMutation,允许您“配置”数据库变异。此事件将在确定应触发数据库变异后触发。

此事件允许您:

  • 重新设置钩子的优先级
  • 禁用钩子

您可以在此事件触发时执行任何操作。

以下示例禁用变异以在删除租户后保留数据库:

namespace App\Listeners;

use Tenancy\Hooks\Database\Events\ConfigureDatabaseMutation;
use Tenancy\Tenant\Events\Deleted;

class ConfigureTenantDatabaseMutations
{
    public function handle(ConfigureDatabaseMutation $event)
    {
        if ($event->event instanceof Deleted) {
            $event->disable();
        }
    }
}

生命周期钩子 - 迁移

概述

此包的目的是处理迁移和数据填充。

要使用此包,需要安装和配置 affects-connections 包。

需要有一个租户 分发生命周期事件 CreatedUpdated 和/或 Deleted

安装

使用 Composer 安装:

composer require tenancy/hooks-migration

注册以下 ServiceProvider:

Tenancy\Hooks\Migration\Provider::class

迁移

配置

安装完包后,需要配置如何迁移租户。可以通过监听 Tenancy\Hooks\Migration\Events\ConfigureMigrations 事件来完成配置。该事件提供了一些简单的功能,让迁移工作更加轻松:

  • path():注册迁移文件所在的路径。这些迁移文件将在迁移时使用。
  • disable():完全禁用迁移。
  • priority():更改迁移的优先级。

警告:迁移的默认优先级是 -50,会在数据库钩子之后运行。如果未禁用迁移或设置优先级小于 -100,则会出现错误,因为数据库已被删除。

示例

以下示例将添加包含应用于租户数据库的迁移文件的 database/tenant/migrations 文件夹。

namespace App\Listeners;

use Tenancy\Hooks\Migration\Events\ConfigureMigrations;

class ConfigureTenantMigrations
{
    public function handle(ConfigureMigrations $event)
    {
        $event->path(database_path('tenant/migrations'));
    }
}

手动运行迁移

如果已经设置了迁移钩子,可以触发 Tenancy\Tenant\Events\Updated 事件来触发钩子并运行迁移:

event(new \Tenancy\Tenant\Events\Updated($tenant));

或者,可以使用 php artisan migrate 命令并传递附加参数:

  • --tenant 参数指定要为其运行迁移的租户。这需要控制台识别
  • --database 参数告诉迁移在 tenant 连接上运行,该连接是使用 affects-connections 动态配置的。
  • --path 参数指定迁移文件的路径。

示例:

php artisan migrate --tenant=1 --database=tenant --path=database/tenant/migrations

数据填充

配置

安装完迁移后,可以使用 Tenancy\Hooks\Migration\Events\ConfigureSeeds 事件来填充租户数据库。该事件有一个非常简单的方法可供使用:

  • seed():接受 Seeder 类的字符串。在下面的示例中可以看到如何使用。
  • disable():完全禁用填充。
  • priority():更改填充的优先级。

注意:填充的默认优先级是 -40,默认在迁移钩子之后运行。

示例

以下示例将使用 ConfigureSeeds 事件添加 UsersSeeder 数据填充类。

namespace App\Listeners;

use Tenancy\Hooks\Migration\Events\ConfigureSeeds;

class ConfigureTenantSeeds
{
    public function handle(ConfigureSeeds $event)
    {
        $event->seed(\UsersSeeder::class);
    }
}

生命周期钩子 - 主机名

概述

此包的目的是在租户更新时运行自定义函数。

要使用此包,需要一个租户,并且租户需要分发 CreatedUpdated 和/或 Deleted生命周期事件

安装

使用 Composer 安装:

composer require tenancy/hooks-hostname

注册以下 ServiceProvider:

Tenancy\Hooks\Hostname\Provider::class

配置

租户

首先,需要更新租户以使用 Tenancy\Hooks\Hostname\Contracts\HasHostnames 契约,并实现 getHostnames 函数以返回租户的主机名数组。

以下示例假设租户有一个 “hostname” 属性:

class Tenant implements Tenancy\Hooks\Hostname\Contracts\HasHostnames
{
    public function getHostnames(): array
    {
        return [
            $this->hostname
        ];
    }
}

处理程序

接下来,需要创建处理程序。处理程序用于执行与租户的主机名相关的所有逻辑,例如注册域名、更新 Nginx/Apache 配置文件等。

要创建处理程序,需要创建一个新的类,该类实现 Tenancy\Hooks\Hostname\Contracts\HostnameHandler 契约,并实现 handle 函数以执行所需的逻辑。

以下示例检查租户的域名是否有效。如果租户没有有效的域名,将向管理员发送电子邮件,告知域名无效。

namespace App\Handlers;

use Tenancy\Hooks\Hostname\Contracts\HostnameHandler;
use Tenancy\Tenant\Events\Event;
use Illuminate\Support\Facades\Mail;

class SimpleHandler implements HostnameHandler
{
    public function handle(Event $event): void
    {
        if (!$this->hasValidDomains($event->tenant)) {
            Mail::to($event->tenant->email)->send(new DomainsNotValid($event->tenant->getHostnames()));
        }
    }
}

钩子

与其他钩子一样,此钩子触发一个简单的事件,允许您正确配置它。该事件是 Tenancy\Hooks\Hostname\Events\ConfigureHostnames,它具有一些非常简单的功能:

  • registerHandler():注册处理程序。
  • getHandlers():获取处理程序。

以下示例注册了前面示例中创建的处理程序:

namespace App\Listeners;

use App\Handlers\SimpleHandler;
use Tenancy\Hooks\Hostname\Events\ConfigureHostnames;

class ConfigureHostnameHandlers
{
    public function handle(ConfigureHostnames $event)
    {
        $event->registerHandler(new SimpleHandler);
    }
}

生命周期钩子 - 自定义钩子

概述

该功能旨在实现通过现有钩子无法实现的自定义功能。

所需条件:

基础

您可以通过实现 Tenancy\Contracts\LifecycleHook 契约来构建自己的钩子。

有两个抽象的钩子类可以简化实现过程:

  • Tenancy\Lifecycle\Hook
  • Tenancy\Lifecycle\ConfigurableHook

基本区别在于,在 Hook 中预期钩子设置不会发生更改,而在 ConfigurableHook 中预期钩子设置是可变的。

例如,一个生命周期钩子在同一默认优先级和默认队列上始终运行,应使用 Hook,而一个将在租户队列上运行的生命周期钩子应使用 ConfigurableHook

实现

只需创建一个包含钩子逻辑的 fire 方法即可实现。

public function fire()
{
    echo "Running hook for " . $this->event->tenant->name;
}

如果钩子需要基于事件实现不同的逻辑(例如创建和删除),可以在 fire 方法中检查事件的类型。

public function fire()
{
    // Created Event
    if ($this->event instanceof \Tenancy\Tenant\Events\Created) {
        dump("Running hook for the creation of a tenant");
        return;
    }
    // Deleted Event
    if ($this->event instanceof \Tenancy\Tenant\Events\Deleted) {
        dump("Running hook for the deletion of a tenant");
        return;
    }
}

如果钩子只需要在一个事件上运行,您可以禁用钩子并在需要触发时启用它。

namespace App\Hooks;

class CustomCreateHook extends ConfigurableHook
{
    public $fires = false;

    public function for($event)
    {
        parent::for($event);

        if ($this->event instanceof \Tenancy\Tenant\Events\Created) {
            $this->fires = true;
        }
        return $this;
    }
}

对于更高级或租户相关的事件更改,请考虑创建一个 事件和监听器 来配置钩子。

区别

HookConfigurableHook 的实现基本相同,除了以下区别。

队列

Hook 默认在队列上运行,并默认使用应用程序的默认队列。

ConfigurableHook 默认不使用队列,只有在指定队列时才会运行在队列上。

// Hook
public $queued = true;
public $queue = null;

// ConfigurableHook
public $queue = null;

触发

Hook 默认始终触发,只要传递的事件是有效的 Tenancy 事件。

ConfigurableHook 默认始终触发。

// Hook
public $event; // 必须是 Tenancy\Tenant\Events\Event 的实例
public $fires = true;

// ConfigurableHook
public $fires = true;

请注意,HookConfigurableHook 都具有包含租户的 event 属性,可以通过 $event->tenant 来访问租户。

注册

有两种方法可以注册自定义的生命周期事件。

扩展 HooksProvider

最简单的方法是创建一个新的服务提供程序(或使用现有的服务提供程序),并使其扩展 Tenancy\Support\HooksProvider。然后创建一个包含您自定义钩子的数组。

namespace App\Providers;

class TenantLifecycleProvider extends Tenancy\Support\HooksProvider
{
    protected $hooks = [
        \App\Hooks\CustomHook::class,
    ];
}

这是注册生命周期事件的推荐方法。

解析解析器

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Tenancy\Lifecycle\Contracts\ResolvesHooks;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->resolving(ResolvesHooks::class, function (ResolvesHooks $resolver) {
            $resolver->addHook(\App\Hooks\CustomHook::class);
        });
    }
}

请注意,这不是注册生命周期事件的推荐方法。

高级用法

ConfiguresHook 事件

建议仅为 ConfigurableHook 钩子实现此功能。

如果要执行高级逻辑以配置钩子,就像可用的钩子一样,可以创建一个新的事件和监听器来配置自定义钩子。为此,请确保您在钩子中覆盖 for 方法。

// Hook
public function for($event)
{
    parent::for($event);
    event(new \App\Events\ConfiguresCustomHookEvent($this->event, $this));
    return $this;
}

示例事件

namespace App\Events;

class ConfiguresCustomHookEvent
{
    /**
     * @var Event
     */
    public $event;

    /**
     * @var ConfigurableHook
     */
    public $hook;

    public function __construct(Event $event, ConfigurableHook $hook)
    {
        $this->event = $event;
        $this->hook = $hook;
    }

    public function disable()
    {
        $this->hook->fires = false;
        return $this;
    }

    public function priority(int $priority = -50)
    {
        $this->hook->priority = $priority;
        return $this;
    }
}

此时,您可以为新事件创建监听器,并按照 Laravel 文档进行注册。

租户识别

概述

租户识别过程决定了您要为哪个租户提供服务。虽然使用 TenantResolver 不是绝对必需的,但这样做会将大部分租户业务逻辑转移到应用程序中。

TenantResolver 分发一些事件,这些事件由识别驱动程序用于识别当前请求的租户。通过使用事件,您不仅限于使用一个驱动程序。每个驱动程序只会在其识别时触发自己的契约。然而,如果触发识别时没有指定特定的驱动程序,它将尝试识别所有驱动程序。第一个响应有效租户对象的驱动程序将导致其他驱动程序被忽略。

在对代码进行任何更改之前,建议您首先阅读整个文档。

无法识别的处理

根据您选择如何构建应用程序,可能会有一些情况下需要在未识别到租户时执行特定的逻辑。要实现此目的,只需为 Tenancy\Identification\Events\NothingIdentified 事件创建一个监听器。

在以下示例中,我们将终止执行并返回 404 错误。

namespace App\Listeners;

use Tenancy\Identification\Events\NothingIdentified;

class NoTenantIdentified 
{
    public function handle(NothingIdentified $event) 
    {
        abort(404);
    }
}
注意:仅当您的应用程序始终期望识别到租户并且没有默认的非租户侧时,才需要执行此操作。

识别驱动程序

每个驱动程序都使用相同的原则。为了让驱动程序使用租户,它必须实现与驱动程序相关的接口(或在 Laravel 术语中称为契约)。

根据您的应用程序,可能需要使用不同的方法来识别租户。以下是可以单独或组合使用的可用识别驱动程序。

识别驱动程序只会在租户模型实现了驱动程序的契约时尝试对其进行识别。

下一步

Affects

Affects通过与框架紧密集成改变了您的 Laravel 应用程序的行为。

HTTP 租户识别

概述

该包的目的是通过 HTTP 请求来识别租户

要求

租户必须在 TenantResolver 中注册。

使用场景

  • 通过子目录路径识别租户
  • 通过子域名识别租户
  • 基于唯一域名识别租户
  • 基于查询参数识别租户
  • 等等

简介

通过 HTTP 请求对象来识别租户是一种非常常见的方式。

要允许通过 Http 驱动程序识别租户,您需要为租户类应用合同并实现所需的方法。

安装

通过 composer 安装:

composer require tenancy/identification-driver-http

注册以下 ServiceProvider:

Tenancy\Identification\Drivers\Http\Providers\IdentificationProvider::class

配置

通过 HTTP 进行标识的租户需要实现 Tenancy\Identification\Drivers\Http\Contracts\IdentifiesByHttp 合同。

tenantIdentificationByHttp 方法应该返回根据当前请求识别出的租户。

以下示例假设您的 Customer 模型具有两个附加列,用于标识其配置的门户。portal_hostname 和 portal_path 允许您将此 Customer 标识为当前主机名和路径请求的租户。

use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Http\Contracts\IdentifiesByHttp;

class Customer extends Model implements Tenant, IdentifiesByHttp
{
  use AllowsTenantIdentification;

    /**
     * Specify whether the tenant model is matching the request.
     *
     * @param Request $request
     * @return Tenant
     */
    public function tenantIdentificationByHttp(Request $request): ?Tenant
    {
        return $this->query()
            ->where('portal_hostname', $request->getHttpHost())
            ->where('portal_path', $request->path())
            ->first();
    }
}

以下示例假设您的 Customer 模型为用户配置使用的子域名添加了额外的列。

use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Http\Contracts\IdentifiesByHttp;

class Customer extends Model implements Tenant, IdentifiesByHttp
{
  use AllowsTenantIdentification;

    /**
     * Specify whether the tenant model is matching the request.
     *
     * @param Request $request
     * @return Tenant
     */
    public function tenantIdentificationByHttp(Request $request): ?Tenant
    {
        list($subdomain) = explode('.', $request->getHost(), 2);
        return $this->query()
            ->where('subdomain', $subdomain)
            ->first();
    }
}

队列租户识别

概述

该包的目的是允许在队列任务中识别租户

要求

租户必须在 TenantResolver 中注册。

使用场景

  • 识别特定作业所属的租户
  • 等等

简介

Laravel 中最强大的功能之一是队列。它们用于运行不需要用户界面并且可以延迟的特定任务。当涉及到多租户时,处理队列可能会很困难,这正是这个包的用武之地。

该包已经为您做了很多工作,如果在队列任务排队时识别出租户,它将记住是哪个租户。如果您想为不同的租户排队一些东西,它还提供了一些强大的覆盖工具。

安装

通过 composer 安装:

composer require tenancy/identification-driver-queue

注册以下 ServiceProvider:

Tenancy\Identification\Drivers\Queue\Providers\IdentificationProvider::class

配置

为了启用租户的队列标识,它必须实现特定的合同 Tenancy\Identification\Drivers\Queue\Contracts\IdentifiesByQueue。在这个函数中,您将获得一个简单的自定义 Processing 事件,其中包含以下两个信息之一:

  • 默认提供的 tenant_keytenant_identifier,如果在排队作业时识别到租户。
  • 您自己提供的 tenanttenant_keytenant_identifier

以下示例中,我们将首先检查是否提供了一个覆盖的租户。如果没有,我们将简单地检查是否可以在此模型中找到租户。

use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\Events\Processing;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Queue\Contracts\IdentifiesByQueue;

class Customer extends Model implements Tenant, IdentifiesByQueue
{
    public function tenantIdentificationByQueue(Processing $event): ?Tenant
    {
        if ($event->tenant) {
            return $event->tenant;
        }

        if ($event->tenant_key && $event->tenant_identifier === $this->getTenantIdentifier()) {
            return $this->newQuery()
                ->where($this->getTenantKeyName(), $event->tenant_key)
                ->first();
        }

        return null;
    }
}

覆盖

有时,您可能希望覆盖租户(例如,当您的管理面板本身是一个租户)。您可以通过为作业提供以下公钥之一来实现这一点:

  • tenant_identifier,应该与租户的 getTenantIdentifier 相同。
  • tenant_key,应该与租户的 getTenantKey 相同。
  • tenant,可以是完整的租户对象。

以下示例展示了一个可覆盖的作业。

namespace App\Jobs;

use App\Models\Customer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Tenancy\Facades\Tenancy;

class MailCustomer implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /** @var Customer $tenant */
    public $tenant;

    public function __construct(Customer $customer, $tenant_key = null, string $tenant_identifier = null)
    {
        $this->tenant = $customer;
    }

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

控制台租户识别

概述

该包的目的是允许通过控制台命令识别租户

要求

租户必须在 TenantResolver 中注册。

使用场景

  • 通过自定义字段识别租户
  • 通过ID识别租户
  • 等等

简介

使用控制台输入对象来识别租户。

为了允许租户通过控制台驱动进行识别,所有命令都增加了两个新选项,--tenant--tenant-identifier

要使用控制台驱动程序识别租户,您需要将合同应用到租户类并实现所需的方法。

安装

确保您使用的模型已在 TenantResolver 中注册。

通过 composer 安装:

composer require tenancy/identification-driver-console

注册以下 ServiceProvider:

Tenancy\Identification\Drivers\Console\Providers\IdentificationProvider::class

配置

需要实现 Tenancy\Identification\Drivers\Console\Contracts\IdentifiesByConsole 合同来识别通过控制台的租户。

tenantIdentificationByConsole 方法应根据当前请求返回已识别的租户。

以下示例假设您的 Customer 模型具有一个 slug 字段,我们将使用该字段来识别租户。

use Illuminate\Database\Eloquent\Model;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Console\Contracts\IdentifiesByConsole;
use Symfony\Component\Console\Input\InputInterface;

class Customer extends Model implements Tenant, IdentifiesByConsole
{
    use AllowsTenantIdentification;

    /**
     * Specify whether the tenant model is matching the request.
     *
     * @param InputInterface $input
     * @return Tenant
     */
    public function tenantIdentificationByConsole(InputInterface $input): ?Tenant
    {
        if ($input->hasParameterOption('--tenant')) {
            return $this->query()
                ->where('slug', $input->getParameterOption('--tenant'))
                ->first();
        }

        return null;
    }
}

高级示例

在下面的示例中,我们将进一步扩展前面的示例,假设您有多个租户类型,可能共享相同的 slug。

use Illuminate\Database\Eloquent\Model;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Console\Contracts\IdentifiesByConsole;
use Symfony\Component\Console\Input\InputInterface;

class Customer extends Model implements Tenant, IdentifiesByConsole
{
    use AllowsTenantIdentification;

    /**
     * Specify whether the tenant model is matching the request.
     *
     * @param InputInterface $input
     * @return Tenant|null
     */
    public function tenantIdentificationByConsole(InputInterface $input): ?Tenant
    {
        if ($input->hasParameterOption('--tenant-identifier')) {
            if($input->getParameterOption('--tenant-identifier') != $this->getTenantIdentifier()) {
                return null;
            }
        }
        if ($input->hasParameterOption('--tenant')) {
            return $this->query()
                ->where('slug', $input->getParameterOption('--tenant'))
                ->first();
        }

        return null;
    }
}

用法

–tenant 选项

该选项用于指定特定的租户。

在下面的示例中,我们假设您要列出特定租户的可用路由。

我们还假设所讨论的租户已经实现了前面的示例,并具有 slug 为 “my-first-tenant”。

php artisan route:list --tenant=my-first-tenant

–tenant-identifier 选项

该选项用于指定特定租户类型。

由于租户可以是多个类,当编写自定义命令以执行特定于不同类型租户的特定操作时,此选项很有用,或者当可以通过相同的 --tenant 值识别不同类型的租户时。

自定义命令

在以下示例命令中,我们假设每个租户都有一个 “maintenance” 值,以便在不必关闭整个应用程序的情况下,能够将其重定向到维护页面。

我们还假设您有两个租户:用户和组织。

namespace App\Console\Commands;

use Illuminate\Console\Command;

class TenantMaintenanceCommand extends Command
{
    protected $signature = 'tenant:maintenance';

    protected $description = 'Inverts the Maintenance mode for a tenant';

    /** @var Resolver */
    protected $resolver;

    public function __construct(ResolvesTenants $resolver)
    {
        parent::__construct();
        $this->resolver = $resolver;
    }

    public function handle()
    {
        $this->resolver->getModels()->each(function (string $class) {
            $model = (new $class());

            if ($this->option('tenant-identifier') != $model->getTenantIdentifier()) {
                return;
            }

            $model->all()->each(function ($tenant) {
                $tenant->maintenance = !$tenant->maintenance;
                $tenant->save();
            })

        });
    }
}
用法

为了仅为用户租户运行您的自定义命令,可以执行以下操作:

php artisan tenant:maintenance --tenant-identifier=mysql.users

环境租户识别

概述

该包的目的是允许基于环境来识别租户。

要求

租户必须在 TenantResolver 中注册。

使用场景

  • 通过应用程序运行的环境来识别租户
  • 在演示环境中确保加载特定租户
  • 等等

简介

该包允许基于环境来识别租户。

安装

通过 composer 安装:

composer require tenancy/identification-driver-environment

注册以下 ServiceProvider:

Tenancy\Identification\Drivers\Environment\Providers\IdentificationProvider::class

配置

这允许您根据环境变量设置自己的身份验证要求,例如:

  • Slug:自动生成的人类可读字符串
  • ID:数据库自增ID

以下示例假设您的 Customer 模型具有一个 slug 字段,我们将使用该字段来识别租户。

use Illuminate\Database\Eloquent\Model;
use Tenancy\Identification\Concerns\AllowsTenantIdentification;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Identification\Drivers\Environment\Contracts\IdentifiesByEnvironment;

class Customer extends Model implements Tenant, IdentifiesByEnvironment
{
    use AllowsTenantIdentification;

    /**
     * Specify whether the tenant model is matching the request.
     *
     * @return Tenant
     */
    public function tenantIdentificationByEnvironment(): ?Tenant
    {
        if ($tenant = env('TENANT')) {
            return $this->query()
                ->where('slug', $tenant)
                ->first();
        }

        return null;
    }
}

注意,使用 env() 助手函数在 Laravel 缓存配置时无法正常工作。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 4
mouyong

github.com/plugins-world/LaravelSa... 半个小时内从0创建一个laravel租户项目。

1年前 评论
抄你码科技有限公司 1年前
mouyong (作者) 1年前
DogLoML

mark

1年前 评论

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