whitedigital-eu/entity-resource-mapper-bundle

将 Doctrine 实体对象映射到 API 资源对象并反向映射的包。

0.24.13 2024-07-15 17:40 UTC

README

  1. 通过帮助将 Doctrine 实体对象与 Api Platform 资源对象进行映射,扩展了 Symfony / Api Platform 的功能,并提供其他辅助功能,例如过滤器、JSON 函数等。

  2. 实现 AuthorizationService,该服务集中管理所有授权配置,并提供授权资源的方法

  • 数据提供者 - 集合获取
  • 数据提供者 - 项目获取
  • 数据持久化 - 项目 post/put/patch
  • 数据持久化 - 项目删除
  • EntityToResourceMapper 中的单个资源

要求

PHP 8.1+
Symfony 6.3+

安装

推荐使用 Composer 安装

composer require whitedigital-eu/entity-resource-mapper-bundle

配置

ClassMapper 服务

您应该创建 ClassMapper 服务配置文件,例如

namespace App\Service;

use App\Dto\CustumerDto;
use App\Entity\Customer;

use WhiteDigital\EntityResourceMapper\Mapper\ClassMapperConfiguratorInterface;
use WhiteDigital\EntityResourceMapper\Mapper\ClassMapper;

class ClassMapperConfigurator implements ClassMapperConfiguratorInterface
{
    public function __invoke(ClassMapper $classMapper)
    {
        $classMapper->registerMapping(CustomerResource::class, Customer::class);
        // with Callback - must return true for mapping to be active
        $classMapper->registerMapping(PublicHtmlResource::class, Html::class, callback: static fn (array $context) => !self::isAdmin($context));
        $classMapper->registerMapping(AdminHtmlResource::class, Html::class, callback: static fn (array $context) => self::isAdmin($context));
    }
    
    /**
     * IsAdmin or else IsPublic.
     */
    private static function isAdmin(array $context): bool
    {
        return array_key_exists('request_uri', $context) && str_starts_with($context['request_uri'], '/api/admin');
    }
}

并在您的 services.yaml 文件中将它注册为 ClassMapper 服务的配置器

    WhiteDigital\EntityResourceMapper\Mapper\ClassMapperConfiguratorInterface:
      class: App\Service\ClassMapperConfigurator

此外,您还可以使用 Mapping 属性来注册映射

use App\Dto\CustumerDto;
use Doctrine\ORM\Mapping as ORM;
use WhiteDigital\EntityResourceMapper\Attribute\Mapping;

#[ORM\Entity]
#[Mapping(CustumerDto::class)]
class Customer ...
use WhiteDigital\EntityResourceMapper\Attribute\Mapping;
use App\Entity\Customer;

#[Mapping(Customer::class)]
class CustumerDto ...

过滤器

目前有以下过滤器可用(过滤器的工作方式如 Api Platform 文档中所述,但以下有注释)

  • ResourceBooleanFilter
  • ResourceDateFilter (如果值不是有效的 DateTime 对象,则抛出异常)
  • ResourceEnumFilter (与 SearchFilter 相同,但有明确的文档)
  • ResourceExistsFilter
  • ResourceJsonFilter (新过滤器)
  • ResourceNumericFilter
  • ResourceOrderFilter (允许按 JSON 值排序)
  • ResourceOrderCustomFilter (按自定义 SELECT 字段排序的过滤器,这些字段既不在根别名中,也不在连接中)
  • ResourceRangeFilter
  • ResourceSearchFilter

JSON 函数

以下 PostgreSQL 函数在 Doctrine 中可用,并在 ResourceJsonFilter 和 ResourceOrderFilter 中使用

  • JSONB_PATH_EXISTS(%s, %s) - PostgreSQL 函数 jsonb_path_exists(%s::jsonb, %s)
  • JSON_GET_TEXT(%s, %s) - PostgreSQL 别名 %s->%s
  • JSON_ARRAY_LENGTH(%s) - PostgreSQL 函数 json_array_length(%s)
  • JSON_CONTAINS(%s, %s) - PostgreSQL 别名 %s::jsonb @> '%s'

DBAL 类型

此包包含并自动配置以下 dbal 类型以使用 UTC 时区

  • date
  • datetime
  • date_immutable
  • datetime_immutable

安全性

可用的操作类型

  • AuthorizationService::ALL 包括以下所有内容
  • AuthorizationService::COL_GET 集合 GET
  • AuthorizationService::ITEM_GET 项目 GET
  • AuthorizationService::COL_POST 集合 POST
  • AuthorizationService::ITEM_PATCH 项目 PUT + PATCH
  • AuthorizationService::ITEM_DELETE 项目 DELETE

可用的授权类型

  • GrantType::ALL 资源完全可用
  • GrantType::LIMITED 资源有局限性可用
  • GrantType::NONE 资源不可用

必须实现 AuthorizationService 配置器。

// src/Service/Configurator/AuthorizationServiceConfigurator.php

use WhiteDigital\EntityResourceMapper\Resource\BaseResource;  
use WhiteDigital\EntityResourceMapper\Security\AuthorizationServiceConfiguratorInterface;  

final class AuthorizationServiceConfigurator implements AuthorizationServiceConfiguratorInterface
{
    public function __invoke(AuthorizationService $service): void
    {
        $service->setAuthorizationOverride(static fn (BaseEntity|BaseResource|null $object = null) => 'cli' === strtolower(PHP_SAPI) && 'test' !== $_ENV['APP_ENV']);

        $service->setResources([
            ActivityResource::class => [
                AuthorizationService::ALL => ['ROLE_SUPER_ADMIN' => GrantType::ALL, 'ROLE_KAM' => GrantType::ALL],
                AuthorizationService::COL_GET => [, 'ROLE_JUNIOR_KAM' => GrantType::OWN],
                AuthorizationService::ITEM_GET => [, 'ROLE_JUNIOR_KAM' => GrantType::GROUP],
                AuthorizationService::COL_POST => [],
                AuthorizationService::ITEM_PATCH => [],
                AuthorizationService::ITEM_DELETE => [],
            ]]);
    
        //either mainResource or roles key must be set
        $service->setMenuStructure(
                [
                    ['name' => 'ACTIVITIES',
                        'mainResource' => ActivityResource::class,
                    ],
                    ['name' => 'REPORTS',
                        'roles' => ['ROLE_SUPER_ADMIN', 'ROLE_KAM'],
                    ],
                 ]);
    }
}

将其注册为服务

WhiteDigital\EntityResourceMapper\Security\AuthorizationServiceConfiguratorInterface:
    class: AuthorizationServiceConfigurator

如果设置了 setAuthorizationOverride 闭包,它将使用当前对象(资源或实体)进行调用,如果返回 true,则跳过授权。

使用以下方法

  • 在 DataProvider 中,getCollection
$this->authorizationService->limitGetCollection($resourceClass, $queryBuilder); // This will affect queryBuilder object
  • 在 DataProvider 中,getItem
$this->authorizationService->authorizeSingleObject($entity, AuthorizationService::ITEM_GET); // This will throw AccessDeniedException if not authorized
  • 在 DataPersister 中,persist
$this->authorizationService->authorizeSingleObject($data, AuthorizationService::ITEM_PATCH); // This will throw AccessDeniedException if not authorized
// or
$this->authorizationService->authorizeSingleObject($data, AuthorizationService::COL_POST; // This will throw AccessDeniedException if not authorized
  • 在 DataPersister 中,remove
$this->authorizationService->authorizeSingleObject($data, AuthorizationService::ITEM_DELETE); // This will throw AccessDeniedException if not authorized
  • 在任意 Resource 中,如果您已将其授权定义为 LIMITED,则必须向 BaseResource 类添加属性,以定义资源类中每个类的访问解析器配置
#[AuthorizeResource(accessResolvers: [
    new AccessResolverConfiguration(className: OwnerPropertyAccessResolver::class, config: ['ownerPropertyPath' => 'supervisor']),
])]

同一类还必须使用以下属性设置正确的规范化组

    #[Groups('deal_read')]
    #[ApiProperty(attributes: ["openapi_context" => ["description" => "If Authorization GrantType::OWN or GROUP is calculated, resource can be restricted."]])]
    public bool $isRestricted = false;

属性可见性检查

有时您希望返回端点中的所有项目,但要根据用户角色限制返回的属性。为此,您需要将 GrantType::LIMITED 设置为希望进行此可见性检查的角色和操作,并将 #[VisibleProperty] 属性添加到需要进行此检查的资源。 #[VisibleProperty] 属性接受两个参数: ownerPropertypropertiesproperties 是您希望 SHOW 的所有属性的数组。ownerProperty 是要检查与当前登录用户的属性名称。

重要:如果资源对某些角色设置了 GrantType::LIMITED 以进行获取或获取集合操作,则至少必须设置一个访问解析器或 #[VisibleProperty]

显式检查是否在授权服务中配置了所有角色

如果您想显式检查是否在授权服务中完全配置了所有项目定义的角色,可以通过传递包含所有所需角色的 BackedEnum 来配置此检查。

默认值是 [],因此没有此配置检查将不会被触发。

<?php declare(strict_types = 1);

use App\Constants\Enum\Roles;
use Symfony\Config\EntityResourceMapperConfig;

return static function (EntityResourceMapperConfig $config): void {
    $config
        ->rolesEnum(Roles::class);
};

或者

entity_resource_mapper:
    roles_enum: App\Constants\Enum\Roles

此枚举必须支持并包含所有所需的角色,并带有 ROLE_ 前缀,如下所示

<?php declare(strict_types = 1);

namespace App\Constants\Enum;

enum Roles: string
{
    case ROLE_USER = 'ROLE_USER';
    case ROLE_ADMIN = 'ROLE_ADMIN'
}

现在,如果您在 AuthorizationService->setServices() 中为任何资源操作配置了 ROLE_USER 或 ROLE_ADMIN 授权,则会抛出异常。

公共资源访问

如果需要未经授权访问任何资源(默认情况下这是禁止的),您可以使用 AuthorizationServiceConfigurator 允许为 AuthenticatedVoter::PUBLIC_ACCESS 允许特定操作。为此,使用 GrantType::ALL 配置所需操作。仅允许使用 GrantType::ALL(不允许使用 GrantType::LIMITED),并且您不需要为公共访问设置 GrantType::NONE。示例

// src/Service/Configurator/AuthorizationServiceConfigurator.php

use WhiteDigital\EntityResourceMapper\Security\AuthorizationServiceConfiguratorInterface;
use WhiteDigital\EntityResourceMapper\Security\Enum\GrantType;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;

final class AuthorizationServiceConfigurator implements AuthorizationServiceConfiguratorInterface
{
    public function __invoke(AuthorizationService $service): void
    {
        $service->setResources([
            ActivityResource::class => [
                AuthorizationService::ALL => ['ROLE_SUPER_ADMIN' => GrantType::ALL, 'ROLE_KAM' => GrantType::ALL],
                AuthorizationService::COL_GET => ['ROLE_JUNIOR_KAM' => GrantType::LIMITED],
                AuthorizationService::ITEM_GET => [AuthenticatedVoter::PUBLIC_ACCESS => GrantType::ALL],
                AuthorizationService::COL_POST => [],
                AuthorizationService::ITEM_PATCH => [],
                AuthorizationService::ITEM_DELETE => [],
            ]]);
    }
}

菜单构建器

此软件包附带菜单构建器功能,允许定义整体菜单结构,并允许根据当前用户限制(授权、规则)动态构建菜单。

要使用菜单构建器服务,您必须首先创建一个实现 WhiteDigital\EntityResourceMapper\Interface\MenuBuilderServiceConfiguratorInterface 的配置器类。

use WhiteDigital\EntityResourceMapper\MenuBuilder\Interface\MenuBuilderServiceConfiguratorInterface;
use WhiteDigital\EntityResourceMapper\MenuBuilder\Services\MenuBuilderService;

final class MenuBuilderServiceConfigurator implements MenuBuilderServiceConfiguratorInterface
{
    public function __invoke(MenuBuilderService $service): void
    {
        //either mainResource or roles key must be set
        $service->setMenuStructure(
                [
                    [
                        'name' => 'ACTIVITIES',
                        'mainResource' => ActivityResource::class,
                    ],
                    [
                        'name' => 'REPORTS',
                        'roles' => ['ROLE_SUPER_ADMIN', 'ROLE_KAM'],
                    ],
                 ]);
    }
}

将配置器类注册为服务

WhiteDigital\EntityResourceMapper\MenuBuilder\Interface\MenuBuilderServiceConfiguratorInterface:
    class: MenuBuilderServiceConfigurator

最后,您可以使用 menubuilder 并通过调用 MenuBuilderService 以这种方式检索过滤后的菜单:

use WhiteDigital\EntityResourceMapper\MenuBuilder\Services\MenuBuilderService;

class SomeClass
{
    public function someFunction(MenuBuilderService $service): void
    {
        $data = $service->getMenuForCurrentUser();
    }
}

基本提供程序和处理器

在大多数情况下,读取或写入数据库的方法是相同的,因此此软件包提供了 AbstractDataProcessorAbstractDataProvider,它们实现了 api 平台的基础逻辑。此软件包的构建部分也使用这些类进行生成。使用这些抽象可以消除为每个实体/资源重复代码的需要。由于这些是抽象,因此您始终可以在需要时覆盖它们的任何功能。

扩展 API 资源

其他 whitedigital-eu 软件包可能附带 API 资源,这些资源在配置某些配置后可能不适合直接在项目中使用。这就是为什么 ExtendedApiResource 对覆盖默认属性中定义的部分非常有用。

例如,看一下 WhiteDigital\Audit\ApiResource\AuditResource 类。它定义了 API 资源。如果您想将其 iri 设置为 /api/vendor/audits,您必须执行以下操作:

  1. 创建一个新的类,该类扩展了您想要覆盖的资源
  2. 添加 ExtendedApiResouce 属性而不是 ApiResource 属性
  3. 仅传递您想要覆盖的选项,其他选项将从扩展的资源中获取
namespace App\ApiResource;

use WhiteDigital\EntityResourceMapper\Attribute\ExtendedApiResource;

#[ExtendedApiResource(routePrefix: '/vendor')]
class AuditResource extends WhiteDigital\Audit\ApiResource\AuditResource
{
}

ExtendedApiResouce 属性检查您正在扩展的资源,并覆盖了在扩展中给出的选项,同时保持其他选项与父资源中的选项相同。

重要:您需要使用api_platform.openapi.factory装饰器禁用捆绑资源,否则您将有两个审计资源实例:一个带有/api/auditsiri,另一个带有/api/vendor/auditsiri。

ApiResource生成器

默认配置选项基于api-platformsymfony推荐,但您可以像这样覆盖它们(显示默认值)

api_resource:
    namespaces:
        api_resource: ApiResource
        class_map_configurator: Service\\Configurator # required by whitedigital-eu/entity-resource-mapper-bundle
        data_processor: DataProcessor
        data_provider: DataProvider
        entity: Entity
        root: App
    defaults:
        api_resource_suffix: Resource
        role_separator: ':'
        space: '_'
use Symfony\Config\EntityResourceMapperConfig;

return static function (EntityResourceMapperConfig $config): void {
    $namespaces = $config
        ->namespaces();

    $namespaces
        ->apiResource('ApiResource')
        ->classMapConfigurator('Service\\Configurator') # required by whitedigital-eu/entity-resource-mapper-bundle
        ->dataProcessor('DataProcessor')
        ->dataProvider('DataProvider')
        ->entity('Entity')
        ->root('App');
        
    $defaults = $config
        ->defaults();
        
    $defaults
        ->apiResourceSuffix('Resource')
        ->roleSeparator(':')
        ->space('_');
};

namespaces用于设置生成的文件的目录。因此,如果您需要将文件放在不同的目录/命名空间中,您可以这样更改它。

roleSeparator和来自defaultsspace用于配置api资源中使用的分隔符。例如,默认情况下,带有默认值的UserRole将变为read组的user_role:read
apiResourcrSuffix定义api资源类名的后缀。例如,默认情况下,User实体将生成UserResource api资源类。

用法

只需运行make:api-resource <实体名>,其中实体名是要为其创建api资源的实体。例如,运行make:api-resource User来为User实体创建UserResource,UserDataProcessor和UserDataProvider。

生成器命令根据实体变量生成资源属性。这有时可能是错误的或不必要的,因此您可以传递--no-properties选项来不生成属性。
默认情况下,生成器命令在尝试生成已存在的类时会抛出错误。如果您出于某些原因想要重写生成的类,可以传递--delete-if-exists选项。
有时这个选项很有用,当您有两个具有关系的实体。由于特定的逻辑不可能,要自动为两个类生成资源,您应该

  1. 运行make:api-resource Entity1 --no-properties
  2. 运行make:api-resource Entity2
  3. 运行make:api-resource Entity1 --delete-if-exists

此命令自动为给定实体生成ApiFilters。默认值为生成第一级字段。例如

use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use WhiteDigital\EntityResourceMapper\Filters\ResourceDateFilter;
use WhiteDigital\EntityResourceMapper\Resource\BaseResource;

#[
    ApiResource (
        shortName: 'User'
    ),
    ApiFilter(ResourceDateFilter::class, properties: ['createdAt', 'updatedAt', ]),
]
class UserResource extends BaseResource 
{
    public ?DateTimeImmutable $createdAt = null;

    public ?DateTimeImmutable $updatedAt = null;
    
    public ?UserResource $parent = null;
}

如果您不想生成任何过滤器,请通过传递level 0来运行命令

bin/console make:api-resource User --level 0

如果您想为子资源生成更多级别的过滤器,例如parent.createdAt,请传递更高级别

bin/console make:api-resource User --level 2
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use WhiteDigital\EntityResourceMapper\Filters\ResourceDateFilter;
use WhiteDigital\EntityResourceMapper\Resource\BaseResource;

#[
    ApiResource (
        shortName: 'User'
    ),
    ApiFilter(ResourceDateFilter::class, properties: ['createdAt', 'updatedAt', 'parent.createdAt', 'parent.updatedAt']),
]
class UserResource extends BaseResource 
{
    public ?DateTimeImmutable $createdAt = null;

    public ?DateTimeImmutable $updatedAt = null;
    
    public ?UserResource $parent = null;
}

更高级别 -> 更深的子资源过滤器。
很明显,您可能不需要所有生成的过滤器,但删除它们比添加它们要容易。

如果您想排除特定类型的过滤器,可以传递--exclude-<filter>来跳过这些过滤器的生成。

bin/console make:api-resource User --level 2 --exclude-array --exclude-numeric --exclude-range

可用的过滤器包括

  • arrayResourceJsonFilter
  • boolResourceBooleanFilter
  • dateResourceDateFilter
  • enumResourceEnumFilter
  • numericResourceNumericFilter
  • rangeResourceRangeFilter
  • searchResourceSearchFilter

ResourceOrderFilter由非排除的numericsearchdatearray过滤器创建。

PHP CS Fixer

重要:在运行php-cs-fixer时,请确保不要格式化skeleton文件夹中的文件。否则,生成器命令将停止工作。

验证器

此库包含Classifiers的验证器。这些验证器仅在实体/资源具有以下结构(不少于此)时才起作用:实体

use Doctrine\ORM\Mapping\Entity;

#[Entity]
class Classifier
{
    private ?int $id = null;
    private ?string $value = null;
    private ?array $data = [];
    private ?ClassifierType $type = null;
}

资源

use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
class ClassifierResource
{
    public mixed $id = null;
    public ?string $value = null;
    public ?array $data = [];
    public ?ClassifierType $type = null;
}

如这些示例所示,您需要一个受支持的枚举(在这里称为ClassifierType),并对其进行验证。
ClassifierType的示例如下

enum ClassifierType: string
{
    case ONE = 'ONE';
    case TWO = 'TWO';
}

现在您可以使用CorrectClassifierTypeClassifierRequiredDataIsSet验证器

CorrectClassifierType:CorrectClassifierType检查在相关资源中是否给出了正确的Classifier类型

use ApiPlatform\Metadata\ApiResource;
use WhiteDigital\EntityResourceMapper\Validator\Constraints as WDAssert;

#[ApiResource]
class TestResource
{
    #[WDAssert\CorrectClassifierType(ClassifierType::ONE)]
    public ?ClassifierResource $one = null;
}

现在如果传递的资源具有任何其他类型(例如,ClassifierType::TWO),则会抛出错误。

ClassifierRequiredDataIsSet:有时在分类器中可能需要额外数据,这可能是有必要的。为此,可以使用ClassifierRequiredDataIsSet来检查这些数据是否已传递。这用于ClassifierResource

use ApiPlatform\Metadata\ApiResource;
use WhiteDigital\EntityResourceMapper\Validator\Constraints as WDAssert;

#[ApiResource]
#[WDAssert\ClassifierRequiredDataIsSet(ClassifierType::ONE, ['test1'])]
class ClassifierResource
{
    public mixed $id = null;
    public ?string $value = null;
    public ?array $data = [];
    public ?ClassifierType $type = null;
}

现在当创建一个类型为ONE的新分类器时,如果数据不包含键为test1的值,将会抛出错误。

测试

通过以下方式运行测试

$ vendor/bin/phpunit

待办事项

  • 性能改进
  • 在dataprovider上的显式连接
  • 计算属性作为查询构建器方法