rentpost / doctrine-multi-tenancy
高级 Doctrine2 多租户扩展
Requires
- php: >=7.4
- doctrine/orm: ^2.10
README
Doctrine 2 扩展,提供高级多租户支持。该扩展的目的是允许在实体级别以及上下文中灵活定义多租户的实现方式。
为什么?
通常,根据不同的业务需求,多租户的处理方式会有所不同。也许每个用户都有不同的角色,或者属于多个组织等。
现在,一般来说,你可以在仓库中处理这些需求,如果你的业务逻辑允许这样的组织,你应该考虑这种方法。然而,在许多情况下,这并不总是可行或理想,尤其是在访问关系型实体,尤其是在通过像 GraphQL API 这样的方式公开实体和关系时,用户可以以定义的方式遍历关系。
这种高级的多租户方法旨在解决这些担忧,提供在实体级别上灵活定义如何处理每个上下文中的多租户的能力。
入门
使用以下说明开始使用这个 Doctrine 扩展。
先决条件
此扩展与 Doctrine 2 和 PHP >= 8.1 兼容。
如果你需要 PHP >= 7.4 支持,请使用 1.0.3
,这是最后一个支持它的版本。
安装
composer install rentpost/doctrine-multi-tenancy
设置
为了让此扩展正常工作,你需要将其与 Doctrine 的 EntityManager
和 EventManager
注册。为此,你需要在 Doctrine 的配置和设置中添加以下内容。具体如何操作将取决于你的实现。有关详细信息,请参阅 Doctrine 的安装和配置文档。
use Doctrine\ORM\Configuration; use Doctrine\DBAL\Connection; use App\Adapter\Doctrine\ORM\MultiTenancy\ContextProvider; // Your namespace for ContextProviders use App\Adapter\Doctrine\ORM\MultiTenancy\ValueHolder; // Your namespace for ValueHolders use Rentpost\Doctrine\MultiTenancy\Listener as MultiTenancyListener; $connection = Connection($dbalParams, new MySQLDriver()) $config = new Configuration(); ... $eventManager = $connection->getEventManager(); // Instantiate the MultiTenancy\Listener $multiTenancyListener = new MultiTenancyListener(); // Now add any ValueHolders you wish to use $multiTenancyListener->addValueHolder(new ValueHolder\Company()); $multiTenancyListener->addValueHolder(new ValueHolder\User()); $multiTenancyListener->addValueHolder(new ValueHolder\Role()); // And any contexts you may wish to use $multiTenancyListener->addContextProvider(new ContextProvider\Admin(); $multiTenancyListener->addContextProvider(new ContextProvider\Manager()); $multiTenancyListener->addContextProvider(new ContextProvider\Guest(); // Subscribe the listener to the EventManager now $eventManager->addEventSubscriber($multiTenancyListener); // Add the filter to the EntityManager config $config->addFilter('multi-tenancy', 'Rentpost\Doctrine\MultiTenancy\Filter'); $entityManager = EntityManager::create($connection, $config, $eventManager); // Lastly, you need to be sure you've enabled the filter $entityManager->getFilters()->enable('multi-tenancy');
现在,让我们来分析一下,如果你不熟悉 Doctrine 的配置/设置。根据你的应用程序配置,上述内容可能会有所不同。这里不会详细介绍 Doctrine 的配置细节。
你需要关注的第一部分是订阅监听器到 EventManager
。如果你不想有任何 ValueHolders
或 ContextProviders
,你可以完全跳过此步骤,只添加过滤器。假设你想要使用这两个。
什么是 ValueHolder?
ValueHolder
是一个实现了 Rentpost\Doctrine\MultiTenancy\ValueHolderInterface
的类。ValueHolder
的主要目的是为给定的 "标识符" 定义一个值。
在上面的配置中,我们为 Company
、User
和 Role
添加了 ValueHolder
。这些将提供你想要在 SQL 查询中使用的参数和值。《ValueHolderInterface》定义了 2 个方法
public function getIdentifier(): string;
public function getValue(): ?string;
上面的示例中,User
可能会返回 userId
作为 "标识符" 和该用户的 id,表示为字符串。它实际上是一个键/值存储,可以延迟加载,这样值可以改变状态。
这将在下面的示例属性中更加清晰。
什么是 ContextProvider?
ContextProvider
是一个实现了 Rentpost\Doctrine\MultiTenancy\ContextProviderInterface
接口的类。主要目的是定义“上下文”,并提供一种方式来验证当前上下文是否处于“上下文”或“上下文相关”状态。
例如,“上下文”可能是用户的“角色”,或者是一个授权级别,或者可以是任何适合您业务逻辑的用途。它旨在灵活,以便适应任何数量的用例。
在上面的配置示例中,我们为 Admin
、Manager
和 Guest
添加了 ContextProvider
。这些 ContextProvider
每个都会公开一个“上下文”。
ContextProviderInterface
定义了 2 个方法
public function getIdentifier(): string;
public function isContextual(): bool;
使用上面提到的 Admin
示例,我们可能返回 admin
作为“标识符”。isContextual
方法负责确定这个特定的 admin
标识符是否被认为是上下文中的,或上下文相关的。在这种情况下,您可能会使用一个具有 isAdmin
方法的 User
对象来构建此类。
与 ValueHolder
一样,以下示例属性将使这一切更加清晰。
用法
在设置好一切之后,困难的部分就过去了。花时间正确评估如何设置您的 ValueHolder
和 ContextProvider
类将大大有助于使使用简单明了。
示例
首先有一些需要注意的事项。
$this
代表 Doctrine 中定义的当前表的别名。ValueHolder
的“标识符”用大括号括起来,例如{myIdentifier}
。- 可以应用多个过滤器。添加多个过滤器将执行所有“在上下文”中的过滤器。
- 没有明确定义上下文的过滤器,即使您已添加
ContextProvider
,也将应用于所有上下文。基本上,它将始终执行。 - 可以为过滤器定义多个“上下文”。如果定义的任何上下文是“上下文相关的”,则将应用过滤器。
无上下文的简单示例
在这个例子中,假设 Product
表有一个名为 company_id
的列,该列用于多租户将产品与特定公司相关联。这里的 {companyId}
参数在上述示例配置中的 ValueHolder\Company
中定义。companyId
将是“标识符”,值将是当前公司的 id。
use Doctrine\ORM\Mapping as ORM; use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy; #[ORM\Entity] #[MultiTenancy(filters: [ new MultiTenancy\Filter(where: '$this.company_id = {companyId}'), ])] class Product { // Whatever }
具有多个过滤器和上下文的另一个示例
在这个例子中,我们添加了多个过滤器。第一个过滤器始终会被应用。第二个过滤器,具有“manager”上下文,只有当在相应的 ContextProvider
中定义的“标识符”,即 manager
,通过 isContextual()
方法被认为是“上下文相关的”时,才会被应用。如果是这样,它也会被应用。
在第二个过滤器中,product
表没有访问我们需要的必要信息来正确应用多租户过滤。因此,我们执行一个子查询。这允许我们对关系表执行查询。在这种情况下,我们实际上是在说“manager”上下文只能访问状态为“已发布”的 product_group
中的 Product
。如果不是这样,则不会返回 Product
。
use Doctrine\ORM\Mapping as ORM; use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy; #[ORM\Entity] #[MultiTenancy(filters: [ new MuiltiTenancy\Filter(where: '$this.company_id = {companyId}'), new MultiTenancy\Filter( context: ['visitor'], where: '$this.id IN( SELECT product_id FROM product_group WHERE status = 'published' )' ), ])] class Product { // Whatever }
具有多个上下文过滤器和策略的示例
在先前的示例中,我们一直使用默认的 FilterStrategy::AnyMatch
,这意味着任何评估为真的上下文都会应用其 where 子句。在这个例子中,您可以看到我们应用了 FilterStrategy::FirstMatch
,这意味着将只应用上下文相关的过滤器。
请注意,如果您没有提供上下文,则假定它是上下文相关的,并且将应用该过滤器,这意味着后续的过滤器将永远不会被评估。
use Doctrine\ORM\Mapping as ORM; use Rentpost\Doctrine\MultiTenancy\Attribute\MultiTenancy; #[ORM\Entity] #[MultiTenancy( strategy: MultiTenancy\FilterStrategy::FirstMatch, filters: [ new MuiltiTenancy\Filter( context: ['admin'] where: '$this.company_id = {companyId}'), new MultiTenancy\Filter( context: ['other'], ignore: true, ), new MultiTenancy\Filter( context: ['visitor'], where: '$this.id IN( SELECT product_id FROM product_group WHERE status = 'published' )' ), ], )] class Product { // Whatever }
关于上述示例,还有一点需要注意,那就是ignore
参数。这个参数允许您指定一个不应用过滤器的上下文。这在与FilterStrategy::FirstMatch
策略结合使用时尤其有用。这两个的结合允许您完全或选择性地忽略实体的多租户性——针对特定上下文而言。
问题 / 缺陷 / 问题
如果您有任何疑问或问题,请随时对此存储库提出问题。
贡献
欢迎新的贡献者加入本项目。如果您有兴趣贡献,请发送一封礼貌的电子邮件至dev@rentpost.com。
作者和维护者
Jacob Thomason jacob@rentpost.com
许可证
本库在MIT许可证下发布。