somnambulist/laravel-doctrine-tenancy

一个使用Laravel和Doctrine的多租户实现。

1.0.1 2021-02-20 18:03 UTC

This package is auto-updated.

Last update: 2024-09-20 00:46:23 UTC


README

GitHub Actions Build Status Issues License PHP Version Current Version

这个库为复杂的多租户应用程序提供了必要的基础设施。多租户允许应用程序通过某种租户标识符被隔离到受保护的区域。这可能是通过子域名、URL参数或其他方案。

术语

租户

标识用户所属的当前账户。有两个组成部分

  • 租户所有者:tenant_owner_id
  • 租户创建者:tenant_creator_id

租户所有者是根账户,实际“拥有”租户感知数据的账户。

租户创建者是添加或操作属于租户所有者数据的实例。

租户所有者和创建者可能是同一实体。

租户是其自己的对象,在容器中注册为:auth.tenant。

租户参与者

租户参与者标识实际提供租户引用的实体。为此库工作,必须定义此实体,并且只能有一个实体。

通常这将是Account类或User或(来自laravel-doctrine/acl)的组织。

租户参与者可能是一个多态实体,例如:使用单表继承。

租户参与者映射

为租户参与者提供别名,以便更容易引用。

注意:这不仅仅是一个容器别名,内部用于标记路由。例如:参与者的类是 \App\Entity\SomeType\TheActualInstanceClass,在路由中我们希望限制到此类型。而不是使用整个类名,它可以别名为“short_name”。

租户感知

实现租户感知契约(接口)的实体。这允许数据由租户所有者/创建者分割。

租户感知实体需要

  • 获取/设置租户所有者ID
  • 获取/设置租户创建者ID
  • 从...导入租户

租户感知存储库

一个特定的存储库,将强制执行租户要求,确保任何获取请求都将正确地与租户所有者和创建者绑定,具体取决于租户所有者数据的已实施安全方案。

租户感知存储库通常封装标准实体存储库类。这可能是标准的Doctrine EntityRepository。

安全模型

定义了数据如何在租户所有者账户内共享。在许多情况下,这将是仅限于租户所有者和创建者,但是这个库允许层次结构,并且用户可以与多个租户相关联。在这种情况下,安全级别将确定用户根据其当前创建实例可以访问哪些信息。

提供的安全模型有

  • 共享 - 租户所有者内的所有数据都共享给所有租户创建者
  • 用户 - 用户可以访问他们在租户所有者内允许访问的所有数据
  • 关闭 - 仅允许所有者中的当前创建者
  • 继承 - 委托给父级以获取安全模型。

可以实现其他模型。默认配置是关闭的,不共享。

注意:要实现自己的安全模型,创建一个替代的SecurityModel类。枚举对象不能扩展。

域感知租户参与者

一个具有域意识的租户参与者向接口添加了对域名的支持。这允许从当前传递到应用程序的主机名解析租户信息。这用于与TenantSiteResolver中间件一起使用。

域意识租户参与者存储库

域意识租户参与者的存储库。它与租户参与者分开,允许使用单独的实例。域意识与TenantSiteResolver中间件一起使用。

租户形式

该库提供以下租户设置,按复杂度递增顺序排列

  • 多账户(单个App),URI租户
  • 多站点,域名租户
  • 多站点和多账户租户

多账户,URI租户

最简单的情况是一个具有多账户租户的单个App。所有用户都必须注册,租户由路由URI中的tenant_creator_id定义。租户在用户登录时解析,这意味着这将对您的应用程序影响最小。

如果您需要提供静态的非租户页面或您的应用程序不需要主题支持,这是首选的租户模型。

多站点,域名租户

在复杂度上增加,下一个级别是基于域名的租户。多个站点从单个应用程序文件夹中运行。这通常是某种形式的白标设置,即同一个应用程序通过不同的品牌重新设计,但底层应用程序实际上相同。

注意:这比单个应用程序租户困难得多。您需要更改/bootstrap/app.php中的Application实例以使用

Somnambulist\Tenancy\Foundation\TenantAwareApplication

注意:您必须删除RouteServiceProvider并添加TenantRouteResolver中间件。

注意:您必须确保您使用的任何缓存都可以处理每个站点的缓存。

此外,这种租户形式需要在任何用户登录或主应用程序实际运行之前始终运行一个中间件来解析当前租户信息。如果使用数据库作为租户源,这可能增加站点开销,因此强烈建议在生产环境中使用高性能缓存,例如:APCu或请求之间的持久内存缓存,以减少租户解析的开销。

可以轻松创建文件系统存储库来替代使用数据库,或者结合使用两者,在租户源更改时生成缓存文件。

可以通过添加文件到您的路由文件夹中,使用域名来为每个站点定制路由。可以通过将域名添加到tenancy.php配置文件下的不可忽略列表中忽略域名后缀:tenancy.multi_site.ignorable_domain_components。默认值是dev.和test。

在几个位置搜索路由

  • routes/<creator_domain>
  • app/Http/<creator_domain>
  • routes/<owner_domain>
  • app/Http/<owner_domain>
  • routes/routes
  • routes/web
  • routes/api
  • app/Http/routes
  • app/Http/web
  • app/Http/api

可以与所有站点共享单一组路由。如果既不存在app/Http也不存在routes,则不会加载任何路由,并抛出异常,显示尝试的路径。

在多站点中,必须更改您的应用程序配置

  • view.paths:应将默认路径更改为views/default
  • view.compiled:应将默认路径更改为views/default

创建应用程序时,您需要创建一个“默认”视图主题,然后为应用程序提供的每个域名镜像此主题。视图文件夹应使用绑定到租户的域名命名。

www.example.com -> resources/views/www.example.com

您的视图文件夹最终将看起来像

resources/views/default
resources/views/www.example.com
resources/views/store.example2.com
resources/views/store.example3.com

一旦解析了租户信息,容器配置就会进行多项更新

  • app.url 替换为当前主机域名(非解析域名)
  • 模板路径按层次重新计算,查找器重置

模板路径顺序重置为

  • 租户创建者域名
  • 租户所有者域名(如果不同)
  • 默认/现有路径

这样模板应该从最具体的到最不具体的顺序进行评估。

注意: auth.tenant 初始化为租户所有者/创建者和一个 NullUser。

多站点与多账户租户

注意:这是最复杂的场景。需要 TenantAwareApplication。

注意:您必须删除RouteServiceProvider并添加TenantRouteResolver中间件。

注意:您必须确保您使用的任何缓存都可以处理每个站点的缓存。

这是两种方法的组合,其中每个多站点都有多个租户。在此配置中,除非进行自定义实现,否则在安全性方面存在限制

  • 每个域名只有一个租户所有者
  • 所有租户所有者都应采用封闭的安全模型
  • 所有租户创建者都应采用封闭的安全模型

允许进一步的租户划分是可能的,但是这需要自定义实现,因为您的租户创建者必须允许子租户并实现适合这种情况的安全模型。一个可能的例子是通过父级级联设置租户所有者(这将是域名租户所有者)。

这种设置对站点性能影响最大,需要用户登录才能解决他们的租户问题。因此,这本质上会导致双重租户解决。

不建议使用此设置,因为它可能导致难以诊断的问题,但包含在内,因为当前实现技术上可行。

注意: auth.tenant 初始化为租户所有者/创建者和一个 NullUser,但在用户认证后会更新为当前认证用户以及根据需要更改的创建者租户。所有者租户应该与创建者相同,因为创建者必须是所有者的子级。

需求

  • PHP 7.3+
  • Laravel 7+
  • laravel-doctrine/orm

安装

使用 composer 安装,或从 github.com/checkout/pull 文件。

  • composer install somnambulist/laravel-doctrine-tenancy

设置/入门

  • Somnambulist\Tenancy\TenancyServiceProvider::class 添加到您的 config/app.php
  • Somnambulist\Tenancy\EventSubscribers\TenantOwnerEventSubscriber::class 添加到 config/doctrine.php 订阅者
  • 创建或导入 config/tenancy.php 文件
  • 创建您的 TenantParticipant 实体/存储库并将其添加到配置文件中
  • 在配置文件中创建您的参与者映射(至少 class => class)
  • 创建具有租户支持的用户 User
  • 创建一个 App\Http\Controller\TenantController 来处理各种租户重定向
  • 添加基本路由
  • 对于多站点
    • 在 bootstrap/app.php
      • 将 Application 实例更改为 Somnambulist\Tenancy\Foundation\TenantAwareApplication
      • 注意:如果启用多站点且未进行此更改,将引发异常。
    • 在 HttpKernel
      • TenantSiteResolver 中间件添加到中间件,在 CheckForMaintenanceMode 之后
      • TenantRouteResolver 中间件添加到中间件,在 TenantSiteResolver 之后
      • 从 config/app.php 中删除 RouteServiceProvider
  • 对于标准应用程序租户和/或对于多站点内的租户
    • AuthenticateTenant 添加为 HttpKernel 路由中间件的 auth.tenant
    • EnsureTenantType 添加为 HttpKernel 路由中间件的 auth.tenant.type

示例用户

以下是一个具有单个租户的租户感知用户的示例

<?php
namespace App\Entity;

use Somnambulist\Tenancy\Contracts\BelongsToTenant as BelongsToTenantContract;
use Somnambulist\Tenancy\Contracts\BelongsToTenantParticipant;
use Somnambulist\Tenancy\Contracts\TenantParticipant;
use Somnambulist\Tenancy\Entities\Concerns\BelongsToTenant;

class User implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, BelongsToTenantContract, BelongsToTenantParticipant
{
    use BelongsToTenant;

    protected $tenant;

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

    public function getTenantParticipant()
    {
        return $this->tenant;
    }
}

您应该在创建实体时始终设置租户。在先前版本中,有一个事件订阅者用于从当前请求中查找它,但是已经删除,因为租户信息是记录的关键部分,并且始终要求它更安全。

示例租户参与者

以下是一个租户参与者的示例

<?php
namespace App\Entity;

use Somnambulist\Tenancy\Contracts\TenantParticipant as TenantParticipantContract;
use Somnambulist\Tenancy\Entities\Concerns\TenantParticipant;

class Account implements TenantParticipantContract
{
    
    use TenantParticipant;
}

基本路由

两个认证中间件期望以下路由被定义并可用

<?php
// tenant selection and error routes
Route::group(['prefix' => 'tenant', 'as' => 'tenant.', 'middleware' => ['auth']], function () {
    Route::get('select',          ['as' => 'select_tenant',             'uses' => 'TenantController@selectTenantAction']);
    Route::get('no-tenants',      ['as' => 'no_tenants',                'uses' => 'TenantController@noTenantsAvailableAction']);
    Route::get('no-access',       ['as' => 'access_denied',             'uses' => 'TenantController@accessDeniedAction']);
    Route::get('not-supported',   ['as' => 'tenant_type_not_supported', 'uses' => 'TenantController@tenantTypeNotSupportedAction']);
    Route::get('invalid-request', ['as' => 'invalid_tenant_hierarchy',  'uses' => 'TenantController@invalidHierarchyAction']);
});

作为一个单独的块(或在之前的章节内)添加需要租户支持/执行的应用区域。这些路由至少应该以以下前缀开始:{tenant_creator_id}。{tenant_owner_id}可以(首先)使用,这将强制auth.tenant中间件验证创建者属于所有者,以及当前用户是否有权访问创建者。

注意:用户不需要访问租户所有者,访问租户创建者意味着可以访问数据子集的权限。

<?php
// Tenant Aware Routes
Route::group(['prefix' => 'account/{tenant_creator_id}', 'as' => 'tenant.', 'namespace' => 'Tenant', 'middleware' => ['auth', 'auth.tenant']], function () {
    Route::get('/', ['as' => 'index', 'uses' => 'DashboardController@indexAction']);

    // routes that should be limited to certain ParticipantTypes
    Route::group(['prefix' => 'customer', 'as' => 'customer.', 'namespace' => 'Customer', 'middleware' => ['auth.tenant.type:crm']], function () {
        Route::get('/', ['as' => 'index', 'uses' => 'CustomerController@indexAction']);
    });
});

AuthController 更改

当使用租户时,AuthController必须修改以包含重定向服务,以便知道登录成功后要转到哪里。如果您的AuthController是Laravel提供的标准版本,只需添加一个authenticated方法

<?php

class AuthController extends Controller
{
    /**
     * @param Request $request
     * @param User    $user
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    protected function authenticated($request, $user)
    {
        // do post authentication stuff...
        //$user->setLastLogin(Carbon::now());
        //$em = app('em');
        //$em->flush($user);

        // redirect to tenant uri
        return app('auth.tenant.redirector')->resolve($user);
    }
}

此外,如果您允许新用户的注册,现在需要添加对租户组件的支持。这必须通过重写postRegister方法来实现

<?php

class AuthController ...
{
    public function postRegister(Request $request)
    {
        $validator = $this->validator($request->all());

        if ($validator->fails()) {
            $this->throwValidationException(
                $request,
                $validator
            );
        }

        $user = $this->create($request->all());
        Auth::login($user);

        // call into redirector which was previously mapped above
        return $this->authenticated($request, $user);
    }
}

实现者需要决定如何处理新注册,或者是否应该允许这种操作。

租户感知实体

最后,你需要一些真正了解租户的东西!所以让我们创建一个非常基础的客户

<?php
namespace App\Entity;

use Somnambulist\Tenancy\Contracts\TenantAware as TenantAwareContract;
use Somnambulist\Tenancy\Entities\Concerns\TenantAware;

class Customer implements TenantAwareContract
{
    use TenantAware;
}

这创建了一个跟踪租户信息的Customer实体。为了节省输入,这使用了内置的特性。需要一个对应的仓库和Doctrine映射文件。以下是一个示例XML映射文件

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    <entity name="App\Entity\Customer" table="customers" repository-class="App\Repository\CustomerRepository">
        <unique-constraints>
            <unique-constraint xml:id="uniq_customers_uuid" columns="uuid" />
        </unique-constraints>

        <id name="id" type="integer">
            <generator strategy="IDENTITY"/>
            <options>
                <option name="unsigned">true</option>
            </options>
        </id>
        
        <field name="uuid" type="guid" />
        <field name="tenantOwnerId" type="integer" />
        <field name="tenantCreatorId" type="integer" />
        <field name="name" type="string" length="255" />
        <field name="createdBy" type="string" length="36" />
        <field name="updatedBy" type="string" length="36" />
        <field name="createdAt" type="datetime" />
        <field name="updatedAt" type="datetime" />
    </entity>
</doctrine-mapping>

租户感知仓库

注意:仅适用于Doctrine。

租户感知仓库简单地将现有的实体仓库包装在标准的仓库接口中。它们应该被定义和创建,因为我们实际上想要将这些作为依赖项注入并设置在容器中。

首先,你需要创建一个App级别的TenantAwareRepository,它扩展

  • Somnambulist\Tenancy\Repositories\TenantAwareRepository

例如

<?php
namespace App\Repository;

use Somnambulist\Tenancy\Repositories\TenantAwareRepository;

class AppTenantAwareRepository extends TenantAwareRepository
{

}

假设你没有自定义安全模型,这应该是一个好主意,再次扩展为具有命名空间的"tenant"仓库,用于我们的客户

<?php
namespace App\Repository\TenantAware;

use App\Repository\AppTenantAwareRepository;

class CustomerRepository extends AppTenantAwareRepository
{

}

现在,可以更新config/tenancy.php以添加仓库配置定义,这样这个类将自动在容器中可用。

注意:这一步假设标准仓库已经使用仓库类作为键映射到容器中。

    [
        'repository' => \App\Repository\TenantAware\CustomerRepository::class,
        'base'       => \App\Repository\CustomerRepository::class,
        //'alias'      => 'app.repository.tenant_aware_customer', // optionally alias
        //'tags'       => ['repository', 'tenant_aware'], // optionally tag
    ],

安全模型

安全模型定义了租户所有者内部数据应该如何共享。默认情况下,不共享任何数据。实际上,只有当User实现了BelongsToTenantParticipants接口,并且一个用户可以有多个租户时,安全模型才适用。

共享

在这种情况下,租户所有者可能会决定所有数据都可以由所有子租户共享。这个模型被称为"共享",意味着租户所有者中的所有数据都可以在任何时间由所有租户创建者访问。

要设置安全模型,只需将TenantParticipant实例保存,并将安全模型设置为:TenantSecurityModel::SHARED()

在幕后,当TenantAwareRepository被查询时,会提取当前租户信息,并将查询构建器实例修改为设置租户所有者或创建者。对于共享数据,只设置所有者。

其他预构建的模型包括

  • user
  • closed
  • inherit

User

User模型限制查询仅限于当前租户所有者和任何映射的租户。因此,如果一个用户有4个子租户,他们只能访问那些4个子租户创建的数据。所有其他数据将被排除。

closed

如果安全模型设置为closed,则所有查询都只创建租户所有者和当前创建者的数据。在这个方案中,即使有多个租户创建者,用户也只能看到由当前创建者创建的数据。

Inherit

继承允许安全模型从父租户中采用。如果父模型是继承的,或者没有父模型,则模型将自动设置为关闭。此库尽可能地倾向于最小权限。

应用/添加安全模型

安全模型规则通过 TenantAwareRepository 内的方法应用。模型名称首字母大写,以 "apply" 开头,以 SecurityModel 结尾,因此 "shared" 变为 "applySharedSecurityModel"。

因此强烈建议使用应用程序级别的存储库,这样您可以通过扩展 TenantSecurityModel、定义一些新的常量,然后在应用程序存储库中添加适当的方法来简单地实现您自己的安全模型。

例如:假设您想有一个“全局”策略,其中所有未拥有的数据都在整个系统中共享,但您还有自己的私有数据,您可以添加一个新的方法

<?php
class AppTenantAwareRepository extends TenantAwareRepository
{

    protected function applyGlobalSecurityModel(QueryBuilder $qb, $alias)
    {
        $qb
            ->where("({$alias}.tenantOwnerId IS NULL OR {$alias}.tenantOwnerId = :tenantOwnerId)")
            ->setParameters([
                ':tenantOwnerId' => $this->tenant->getTenantOwnerId(),
            ])
        ;
    }
}

根据需要可以添加额外的方案。

注意:理论上,您可以在租户内部混合安全模型,例如:一些子项是关闭的,一些是共享的,一些是用户;这可能会导致奇怪的结果或不一致。可能会导致记录重复的大幅增加。您必须相应地管理。

路由

对于租户组内的任何路由,并且假设占位符名称是 tenant_creator_id,为租户控制器生成的任何路由都将自动嵌入当前租户信息。实际上,在创建路由时,会自动检查并注入所有者者和创建者。

这是通过覆盖默认的 UrlGenerator 并添加 Tenant 实体来完成的,然后检查路由信息中的 {tenant_owner_id} 和 {tenant_creator_id}。然后自动注入属性。

这仅在使用命名路由且在租户组内时发生。

对于路径,您必须自己包含租户信息;同样,在创建租户选择列表时,您必须在输出链接时作为参数提供租户信息。

当然,您可以简单地通过在调用 link_to_route 时设置它们来覆盖租户参数。

您应该为租户使用命名路由,因为这使更改路由结构变得更容易。

最后:就像存储库一样,您应该清楚地标记基于租户的路由,以免与标准路由混淆。

此外,任何未经认证的路由应从租户组中排除 - 除非您实现了租户感知匿名用户(不推荐)。

多站点路由

在多站点设置中,您可能希望每个站点有不同的路由。在这种情况下,您需要完全删除您的 RouteServiceProvider 并将其替换为 TenantRouteResolver 中间件。然后,您需要创建每个租户域的路由文件(可以包含共享路由),或者如果您希望使用确切的相同路由,则需要创建符号链接。

提供了一个中间件来处理多站点设置中的路由加载。必须在 TenantSiteResolver 之后,但在任何其他中间件之前加载此中间件。此外,您必须禁用/删除默认的 App/Providers/RouteServiceProvider。此提供者在注册得太早,必须通过 TenantRouteResolver 延迟/解决。

这种设置的目的是确保仅加载所选租户的路由,而不附加到任何现有的路由文件中。

注意:这些不是路由中间件,而是 Kernel 中间件。

您的 Kernel.php 最终看起来像以下这样

<?php
class Kernel extends HttpKernel
{
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,

        // must appear AFTER maintenance mode, but before everything else
        \Somnambulist\Tenancy\Http\Middleware\TenantSiteResolver::class,
        \Somnambulist\Tenancy\Http\Middleware\TenantRouteResolver::class,

        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
    ];

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.tenant' => \Somnambulist\Tenancy\Http\Middleware\AuthenticateTenant::class,
        'auth.tenant.type' => \Somnambulist\Tenancy\Http\Middleware\EnsureTenantType::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    ];
}

在多站点中,auth.tenant / auth.tenant.type 是可选的,并且仅在您使用多账户租户时才应包括在内。

再次:确保在 config/app.php 中已删除以前的 RouteServiceProvider。

注意:在多站设置中,您必须不得使用标准route:list、route:cache命令。如果检测到配置设置中的多站设置,这些命令的租户感知版本将自动注册,并且以tenant:为前缀。

路由命名空间

当使用TenantRouteResolver时,您必须在multi_site配置块下的租户配置文件中指定路由命名空间。

<?php
// config/app.php
return [
    // other stuff...
    'multi_site' => [
        'router' => [
            'namespace' => 'App\Http\Controller', // default
        ],
    ],
    // more stuff...
];

如果省略,则使用默认的App\Http\Controller。如果设置为空字符串,则不会在任何路由上设置命名空间前缀。

路由模式

类似于命名空间,您还可以通过将它们添加到config/tenancy.php下的multi_site.router.patterns来设置模式。这是一个标识符和模式的关联数组。它们在解析路由时与路由器注册。

<?php
// config/app.php
return [
    // other stuff...
    'multi_site' => [
        'router' => [
            'namespace' => 'App\Http\Controller', // default
            'patterns' => [
                'id' => '[0-9]+',
            ],
        ],
    ],
    // more stuff...
];

中间件

AuthenticateTenant

AuthenticateTenant确保当前认证的用户有权访问当前指定的租户URI。它用作路由中间件,并且对于多账户租户系统是必需的。

TenantSiteResolver

TenantSiteResolver将确定请求的主机是否是有效的租户主机。这是主要的多站租户中间件。它必须注册为Kernel中间件,并在维护模式检查之后但在其他任何中间件之前运行。

TenantRouteResolver

TenantRouteResolver是多站中间件的第二部分。它在站点解析器之后运行,并尝试从位于App/Http/的.php文件中加载主机路由信息。如果当前租户不是DomainAwareTenantParticipant,则将检查标准的routes.php文件。

EnsureTenantType

EnsureTenantType是一个路由中间件,当您为租户参与者使用继承时使用。它允许对某些租户类型进行安全保护,例如:您可以标记一组路由需要特定的会员类型,或作为升级服务的机遇,或者纯粹作为安全保护,确保基本租户无法访问管理功能。

此中间件应该是租户中间件中最后运行的。

Twig扩展

提供了一个可以添加到config/twigbridge.php扩展的Twig扩展。这添加了以下模板函数

  • current_tenant_owner_id
  • current_tenant_creator_id
  • current_tenant_owner
  • current_tenant_creator
  • current_tenant_security_model

这允许访问当前解析的租户实例。要启用Twig扩展,请将其添加到config/twigbridge.php文件中的扩展列表中。

注意:在之前的迭代中,这包括从存储库中查找租户所有者/创建者的函数,然而:由于租户可以是域感知租户或标准租户,您不知道要使用哪个存储库,因此已将其删除。此外,此信息几乎肯定不应在标准视图中被拉取。

视图

捆绑的TenantController期望在以下位置找到视图

  • /resources/views/tenant
  • /resources/views/tenant/error

这些不包括在内,因为它们需要应用程序实现。TenantController类包含有关文件名和路由映射的信息。

在多站中,这些需要在适当的子文件夹中放置/必要时复制。

潜在问题

与多租户一起工作可能非常复杂。这个库在共享数据库上工作,而不是在单个数据库上,然而如果需要,可以根据租户设置特定的数据库(如果您对多个连接/定义在Doctrine中感到舒适)。

创建存储库时,始终确保租户感知/非租户感知类型被清楚地标记,以避免在错误上下文中使用错误类型。最佳情况:您什么也看不到,最坏情况:您看到未过滤的所有内容。

请注意,在这个系统中,通过Doctrines DQL过滤器预先应用了没有魔法的SQL过滤器:这是故意的。您应该能够在任何时候轻松切换租户,这可以通过简单地更新租户实例或使用无租户感知的仓库来完成。

此外:没有租户ID是其他对象的引用。这同样是故意的。例如,它允许客户数据存储在与您的用户分开的数据库中,使其更加便携。

使用租户将在您的应用程序中增加一些开销。这将取决于您有多少数据以及您应用的安全模型。

始终进行测试,并具备功能测试以确保正确应用了租户,并且每当有疑问时:始终拒绝而不是授予访问权限。

链接

其他多租户项目