rinvex/laravel-tenants

Rinvex Tenants是一个针对单数据库多租户的上下文智能多态Laravel包。您可以使用相同的数据库轻松地完全隔离租户数据,同时拥有完全的控制权来决定哪些数据应该集中共享,哪些数据应该是租户相关的,因此与其他数据隔离。

v8.1.2 2023-07-03 13:28 UTC

README

Rinvex Tenants是一个针对单数据库多租户的上下文智能多态Laravel包。您可以使用相同的数据库轻松地完全隔离租户数据,同时拥有完全的控制权来决定哪些数据应该集中共享,哪些数据应该是租户相关的,因此与其他数据隔离。

Packagist Scrutinizer Code Quality Travis StyleCI License

安装

  1. 通过composer安装此包

    composer require rinvex/laravel-tenants
  2. 发布资源(迁移和配置文件)

    php artisan rinvex:publish:tenants
  3. 通过以下命令执行迁移

    php artisan rinvex:migrate:tenants
  4. 完成!

使用

Rinvex Tenants的设计理念是每个可租户模型可以同时附加到多个租户,因此您不需要在模型数据库表中添加特殊列来指定所属租户,租户关系简单地存储在单独的中央表中。

范围查询

为了正确地范围查询,请在主模型上应用`\Rinvex\Tenants\Traits\Tenantable`特性。这将确保对父模型的调用都是针对当前租户的,并且对子关系的调用都是通过父关系进行范围限制的。

namespace App\Models;

use App\Models\Feature;
use Rinvex\Tenants\Traits\Tenantable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Tenantable;

    public function features()
    {
        return $this->hasMany(Feature::class);
    }
}

范围子模型查询

如果您有子模型,例如产品特性,并且这些特性通过关系属于租户可用的产品,您可能还需要对特性模型查询进行范围限制。为此,您需要在子模型上应用`\Rinvex\Tenants\Traits\TenantableChild`特性,并定义一个新的方法`getRelationshipToTenantable`,它返回一个表示父关系的字符串。查看以下示例。

namespace App\Models;

use App\Models\Product;
use Illuminate\Database\Eloquent\Model;
use Rinvex\Tenants\Traits\TenantableChild;

class Feature extends Model
{
    use TenantableChild;

    public function getRelationshipToTenantable(): string
    {
        return 'product';
    }

    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

这将自动将所有`App\Models\Feature::class`查询范围到当前租户。请注意,此方法的限制是需要能够定义与主模型的关系,因此如果您需要在更深层次的子层次结构中这样做,例如`App\Models\Discount::class`模型属于`App\Models\Feature::class`,而`App\Models\Feature::class`属于`App\Models\Product::class`,而`App\Models\Product::class`属于`Rinvex\Tenants\Models\Tenant::class`,您需要定义一些奇怪的关系。Laravel支持HasOneThrough,但不支持BelongsToThrough,因此您可能需要进行一些修改。

管理租户

这里没有什么特别之处,只是正常的Eloquent模型操作

// Create a new tenant
app('rinvex.tenants.tenant')->create([
    'name' => 'ACME Inc.',
    'slug' => 'acme',
    'domain' => 'acme.test',
    'email' => 'info@acme.test',
    'language_code' => 'en',
    'country_code' => 'us',
]);

// Get existing tenant by id
$tenant = app('rinvex.tenants.tenant')->find(1);

注意:由于`Rinvex Tenants`扩展并利用其他优秀的包,请查阅以下文档以获取更多详细信息

自动租户注册

租户在请求开始时非常早的阶段就自动注册到服务容器中,通过服务提供者的`boot`方法实现。

这样您在模型加载、需要作用域或启动特性之前,就能访问当前活跃的租户。这比路由注册和中间件管道都要早,因此您可以确保所有需要作用域的资源都正确地设置了作用域。

更改活跃租户

您可以在请求的任何时刻轻松更改当前活跃的租户,如下所示

    $tenant = app('rinvex.tenants.tenant')->find(123);
    app()->bind('request.tenant', fn() => $tenant);

要停用您的租户并停止通过它进行作用域,只需将相同的容器服务绑定设置为null,如下所示

    app()->bind('request.tenant', null);

注意

  • 同一时间只能有一个租户处于活跃状态,即使您的资源属于多个租户。
  • 您可以在请求的任何时刻更改活跃租户,但新激活的租户将只会对更改后检索的模型进行作用域,而在请求的早期阶段检索的任何其他模型将使用上一个租户进行作用域,或者根本不进行作用域(根据您的逻辑)。
  • 如果资源属于多个租户,您可以通过重新初始化请求在不同租户之间切换。例如:由于租户目前是通过域名或子域名解析的,要切换租户,您需要将用户重定向到新的租户域名/子域名,同时当前活跃的租户也将切换,请求也将自动由新的租户进行作用域。

默认租户解析器

Rinvex Tenants使用解析器类解析当前活跃租户。它提供了一些默认解析器,您可以使用它们,或者您可以根据需要构建自己的自定义解析器以支持额外的功能。

配置选项中的默认租户解析器类

    // Tenant Resolver Class:
    // - \Rinvex\Tenants\Http\Resolvers\DomainTenantResolver::class
    // - \Rinvex\Tenants\Http\Resolvers\SubdomainTenantResolver::class
    // - \Rinvex\Tenants\Http\Resolvers\SubdomainOrDomainTenantResolver::class
    'resolver' => \Rinvex\Tenants\Resolvers\SubdomainOrDomainTenantResolver::class,

默认使用的租户解析器是SubdomainOrDomainTenantResolver::class,因此此包会自动使用域名和子域名解析当前活跃租户。您可以通过配置选项来更改此设置。

中央域名

Rinvex Tenants支持在多个域名上运行您的应用程序,我们称它们为中央域名。它还支持更复杂的用例,但这超出了此包的范围。

因此,此包期望您在您的config/app.php中具有以下配置选项

    'domains' => [
        'domain.net' => [],
        'example.com' => [],
    ],

默认域名

您需要以相同格式添加上述配置选项的原因是,它是为了支持此包未涵盖的更高级用例。如果您需要检查这些用例之一,请转到Cortex Tenants,这是一个实现accessareas概念的模块,允许不同的域名访问不同的accessareas(例如,frontarea、adminarea、managerarea、tenantarea等)。这里的基线是,您需要将上述配置选项添加到您的config/app.php中,并指定所有应用程序域名。

您需要将默认域名添加到域名列表中,因为此包会自动覆盖默认Laravel配置选项app.url与匹配的域名,尽管您可能需要编写一些应用程序逻辑。查看上述提到的Cortex Tenants模块以获取示例。

租户域名

租户可以通过中央子域名(显然是中央域名的子域名)访问,或者通过他们自己的专用域名访问。

例如,如果默认域名为rinvex.com,并且租户slug为cortex,则中央子域名为cortex.rinvex.com

请注意,由于此包支持多个中央域名,因此租户将通过所有中央子域名访问,因此如果我们有另一个别名中央域名rinvex.net,您可以期望cortex也将在cortex.rinvex.net上可用。

租户还可以选择有自己的顶级域名,例如test-example.com,这意味着现在可以通过三个不同的域名访问

  • cortex.rinvex.com
  • cortex.rinvex.net
  • test-example.com

会话域名

由于 Rinvex Tenants 支持多个中心和租户域,它需要动态地更改 Laravel 默认的会话配置,这正是它所做的事情。它将根据当前请求的主机动态更改 session.domain 配置选项。

注意:出于安全原因,通过多个顶级域名(.rinvex.com & .rinvex.net)访问同一应用程序意味着用户将为每个不同的域名登录,因为他们的会话和Cookies与顶级域名绑定。例如:如果用户登录到 cortex.rinvex.com,他们将保持登录到顶级域名 rinvex.com 及其所有子域(如 website.rinvex.com),但他们将不会登录到 cortex.rinvex.net,即使这两个域名都指向同一应用程序,他们也需要在那里再次登录。这是由于浏览器强制实行的安全限制而众所周知的一个限制。我们可能在将来创建一个解决方案,但这有点复杂,涉及到第三方Cookies和CORS,所以如果您有创造性的解决方案,请随时提交PR。

查询租户范围模型

添加租户后,所有针对可租户模型(tenantable Model)的查询都将自动范围化。

// This will only include Models belonging to the currently active tenant
$tenantProducts = \App\Models\Product::all();

// This will fail with a `ModelNotFoundForTenantException` if it belongs to the wrong tenant
$product = \App\Models\Product::find(2);

如果您需要查询所有租户,可以使用 forAllTenants() 方法。

// Will include results from ALL tenants, just for this query
$allTenantProducts = \App\Models\Product::forAllTenants()->get();

在内部,Rinvex Tenants 使用 Laravel 的 全局作用域(Global Scopes),这意味着如果您正在按活动租户进行范围化,并且想要排除单个查询,您可以这样做。

// Will NOT be scoped, and will return results from ALL tenants, just for this query
$allTenantProducts = \App\Models\Product::withoutTenants()->get();

注意

  • 在开发多租户应用程序时,有时您可能会感到困惑,为什么您总是对那些确实存在的行收到 ModelNotFound 异常,因为这些行属于错误的租户。
  • Rinvex Tenants 会捕获这些异常,并将它们重新抛出为 ModelNotFoundForTenantException,以帮助您解决问题。

管理您的可租户模型

API 直观且非常简单,让我们快速浏览一下。

// Get instance of your model
$product = new \App\Models\Product::find(1);

// Get attached tenants collection
$product->tenants;

// Get attached tenants query builder
$product->tenants();

您可以通过多种方式附加租户。

// Single tenant id
$product->attachTenants(1);

// Multiple tenant IDs array
$product->attachTenants([1, 2, 5]);

// Multiple tenant IDs collection
$product->attachTenants(collect([1, 2, 5]));

// Single tenant model instance
$tenantInstance = app('rinvex.tenants.tenant')->first();
$product->attachTenants($tenantInstance);

// Single tenant slug
$product->attachTenants('test-tenant');

// Multiple tenant slugs array
$product->attachTenants(['first-tenant', 'second-tenant']);

// Multiple tenant slugs collection
$product->attachTenants(collect(['first-tenant', 'second-tenant']));

// Multiple tenant model instances
$tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get();
$product->attachTenants($tenantInstances);

注意

  • attachTenants() 方法将给定的租户附加到模型,而不会触及当前附加的租户,而 syncTenants() 方法可以断开不在给定项目中的任何记录的连接,此方法接受一个可选的布尔参数,用于设置断开连接标志为 truefalse
  • 要断开模型租户,您可以使用 detachTenants() 方法,该方法与 attachTenants() 方法的签名完全相同,具有附加功能:通过将 null 或空值传递给该方法来断开所有当前附加的租户,如下所示:$product->detachTenants();

如您所预期的那样,您可以检查租户是否已附加。

// Single tenant id
$product->hasAnyTenants(1);

// Multiple tenant IDs array
$product->hasAnyTenants([1, 2, 5]);

// Multiple tenant IDs collection
$product->hasAnyTenants(collect([1, 2, 5]));

// Single tenant model instance
$tenantInstance = app('rinvex.tenants.tenant')->first();
$product->hasAnyTenants($tenantInstance);

// Single tenant slug
$product->hasAnyTenants('test-tenant');

// Multiple tenant slugs array
$product->hasAnyTenants(['first-tenant', 'second-tenant']);

// Multiple tenant slugs collection
$product->hasAnyTenants(collect(['first-tenant', 'second-tenant']));

// Multiple tenant model instances
$tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get();
$product->hasAnyTenants($tenantInstances);

注意

  • hasAnyTenants() 方法检查 ANY 给定的租户是否附加到模型。它返回布尔值 truefalse
  • 类似地,hasAllTenants() 方法与 hasAnyTenants() 方法的签名完全相同,但它的行为不同,并且执行严格的比较以检查 ALL 给定的租户是否已附加。

高级用法

生成租户别名

Rinvex Tenants 自动生成别名并自动检测并插入默认翻译(如果未提供),但您仍然可以通过正常的 Eloquent create 方法显式地传递它,如下所示。

app('rinvex.tenants.tenant')->create(['name' => ['en' => 'My New Tenant'], 'slug' => 'custom-tenant-slug']);

注意:有关详细信息,请查看 Sluggable 包。

智能参数检测

Rinvex Tenants 接受租户列表的方法足够智能,可以处理您在上面示例中看到的所有类型的输入。它将检查输入类型并根据情况进行处理。

检索附加到租户的所有模型

您可能会遇到需要获取与特定租户关联的所有模型的情况,您可以轻松地按照以下步骤进行操作

$tenant = app('rinvex.tenants.tenant')->find(1);
$tenant->entries(\App\Models\Product::class);

查询作用域

是的,Rinvex租户附带了一些方便的查询作用域,以下为使用示例

// Single tenant id
$product->withAnyTenants(1)->get();

// Multiple tenant IDs array
$product->withAnyTenants([1, 2, 5])->get();

// Multiple tenant IDs collection
$product->withAnyTenants(collect([1, 2, 5]))->get();

// Single tenant model instance
$tenantInstance = app('rinvex.tenants.tenant')->first();
$product->withAnyTenants($tenantInstance)->get();

// Single tenant slug
$product->withAnyTenants('test-tenant')->get();

// Multiple tenant slugs array
$product->withAnyTenants(['first-tenant', 'second-tenant'])->get();

// Multiple tenant slugs collection
$product->withAnyTenants(collect(['first-tenant', 'second-tenant']))->get();

// Multiple tenant model instances
$tenantInstances = app('rinvex.tenants.tenant')->whereIn('id', [1, 2, 5])->get();
$product->withAnyTenants($tenantInstances)->get();

注意

  • withAnyTenants()作用域可以找到具有给定租户所附带的任何租户的产品。它通常返回一个查询构建器,因此您可以链式调用或调用get()方法来执行并获取结果。
  • 类似地,还有一些其他作用域,如withAllTenants()可以找到具有给定所有租户的产品,withoutTenants()可以找到没有给定任何租户的产品,以及最后是withoutAnyTenants(),它可以找到没有任何租户的产品。所有作用域都是相同的,具有相同的签名,并返回查询构建器。

租户翻译

可以轻松地按照以下方式管理租户翻译

$tenant = app('rinvex.tenants.tenant')->find(1);

// Update title translations
$tenant->setTranslation('name', 'en', 'New English Tenant Title')->save();

// Alternatively you can use default eloquent update
$tenant->update([
    'name' => [
        'en' => 'New Tenant',
        'ar' => 'مستأجر جديد',
    ],
]);

// Get single tenant translation
$tenant->getTranslation('name', 'en');

// Get all tenant translations
$tenant->getTranslations('name');

// Get tenant title in default locale
$tenant->name;

注意:有关详细信息,请检查Translatable软件包。

变更日志

有关项目的完整历史记录,请参阅变更日志

支持

以下支持渠道触手可及

贡献与协议

感谢您考虑为这个项目做出贡献!贡献指南可以在CONTRIBUTING.md中找到。

欢迎提交错误报告、功能请求和拉取请求。

安全漏洞

如果您在这个项目中发现了安全漏洞,请发送电子邮件至help@rinvex.com。所有安全漏洞都将得到及时处理。

关于Rinvex

Rinvex是一家成立于2016年6月的开罗,埃及的软件解决方案初创公司,专注于为中小企业提供一体化企业解决方案。我们相信,我们的动力“价值、触达和影响”使我们与众不同,并通过软件的力量释放我们哲学的无限可能性。我们喜欢称之为“生活节奏的创新”。这就是我们为推进人类文明做出贡献的方式。

许可证

本软件根据MIT许可证(MIT)发布。

(c) 2016-2022 Rinvex LLC,部分版权所有。