lshamanl/symfony-ui-bundle

此包已被弃用且不再维护。作者建议使用 intellect-web-development/symfony-presentation-bundle 包。

Symfony API-Adapter, UI-Bundle, Filters

安装: 76

依赖者: 0

建议者: 0

安全性: 0

星星: 0

关注者: 1

分支: 0

开放问题: 0

Type:symfony-bundle

1.1.3 2021-10-15 07:27 UTC

This package is auto-updated.

Last update: 2022-09-18 15:37:22 UTC


README

描述

该包是Symphony-bundle。它实现了CQRS模式,提供了处理Query和Command请求的能力。

此包解决的问题:减少开发者在UI入口(控制器、CommandBus)和Controller中编写重复代码的需求。

为了处理Command,该包提供了一种接口,用于从"Controller"层向"Application(UseCase)"层传递"上下文"。

对于Query,该包的工作是完全自动化的,为了正确地按标识符过滤和选择,只需指定一些配置即可。无需更多的手动SQL查询和手动操作QueryBuilder ;)

外部使用

查询字符串示例

GET /clients?filter[emails.email][like]=26d@&sort=-createdAt,updatedAt&page[number]=1&page[size]=20&filter[userId][eq]=ccf92b7a-8e05-4f4b-9f0a-e4360dbacb23&filter[name.translations.last][eq]=Tesla&lang=ru

合约

排序

描述

排序由"sort"参数设置。排序方向由名称前的可选"-"符号指定。如果存在"-",则按此字段进行降序排序,否则按升序排序。允许对多个聚合字段进行排序。为此,需要写入多个字段,并用逗号分隔。越早指定的字段,在查询时越重要。

示例
sort='-createdAt,updatedAt'

分页

描述

分页由"page"参数设置。参数有两个字段 - number和size。

  • "number"指定客户端请求的页面号。默认:1
  • "size"指定页面大小(应显示多少个聚合)。默认:20
描述
page[number]='1'
page[size]='20'

过滤

描述

搜索运算符

名称 允许值 示例 描述
NOT_IN 'not-in' filter[status][not-in][]='blocked' 属性不包含所列的任何值
IN 'in' filter[status][in][]='active' 属性包含所列的任何值
RANGE 'range' filter[rating][range]='17,42' 属性位于所选的指定范围内
IS_NULL 'is-null' filter[gender][is-null] 属性等于null
NOT_NULL 'not-null' filter[name][not-null] 属性不等于null
LESS_THAN 'less-than', '<', 'lt' filter[rating][<]='94' 属性小于指定值
GREATER_THAN '大于', '>', 'gt' filter[rating][>]='42' 属性大于指定值
LESS_OR_EQUALS '小于等于', '<=', 'lte' filter[rating][<=]='15' 属性小于或等于指定值
GREATER_OR_EQUALS '大于等于', '>=', 'gte' filter[rating][>=]='97' 属性大于等于指定值
LIKE 'like' filter[email][like]='26d@' 属性包含指定值的部分
NOT_LIKE 'not-like' filter[email][not-like]='27d@' 属性不包含指定值的部分
EQUALS 'equals', '=', 'eq' filter[userId][eq]='ccf92b7a-8e05-4f4b-9f0a-e4360dbacb23' 属性等同于指定值
NOT_EQUALS 'not-equals', '!=', '<>', 'neq' filter[userId][neq]='aaf92b7a-8e05-4f4b-9f0a-e4360dbacb23' 属性不等于指定值
示例
filter[userId][eq]='ccf92b7a-8e05-4f4b-9f0a-e4360dbacb23'
filter[name.translations.last][eq]='Tesla'
filter[emails.email][like]='26d@'
filter[userId][eq]='ccf92b7a-8e05-4f4b-9f0a-e4360dbacb23'
filter[name.translations.last][eq]='Tesla'
filter[emails.email][in][]='0791d11b6a952a3804e7cb8a220d0a9b@mail.ru'
filter[emails.email][in][]='0891d11b6a952a3804e7cb8a220d0a9b@mail.ru'

定义

InputContract

描述

InputContract 是应用程序入口点的 DTO 描述。所有 DTO 字段都必须是标量类型。可以包含 "Validation Asserts"。可用于生成自动文档。

目的

序列化和验证来自 Request 的数据,生成自动文档。

示例

<?php

declare(strict_types=1);

namespace Path\To\Class;

use Bundle\UIBundle\Core\Contract\Command\InputContractInterface;
use Symfony\Component\Validator\Constraints as Assert;

class Contract implements InputContractInterface
{
    #[Assert\Uuid]
    #[Assert\NotBlank]
    public string $userId;

    #[Assert\Email]
    #[Assert\NotBlank]
    public string $email;
}

Command

描述

Command 是一个 DTO,它包含从 InputContract 验证过的数据。区别在于该类型的 DTO 不仅包含标量类型,还可以包含 ValueObject。因此,不能用于生成自动文档。

目的

将准备好的、分组的数据传递到 Handler(UseCase)。

示例

<?php

declare(strict_types=1);

namespace Path\To\Class;

use App\Path\To\Entity\User\ValueObject\Id as UserId;
use Bundle\UIBundle\Core\Contract\Command\CommandInterface;

final class Command implements CommandInterface
{
    public string $email;
    public UserId $userId;
}

Handler

描述

Handler 是服务场景。通常有一个接受 "CommandDto" 作为参数的 "handle" 方法。

目的

执行服务场景

示例

<?php

declare(strict_types=1);

namespace Path\To\Class;

use App\Model\Flusher;
use App\Path\To\Entity\Client;
use App\Path\To\Entity\ClientRepository;
use Bundle\UIBundle\Core\Contract\Command\CommandInterface;
use Bundle\UIBundle\Core\Contract\Command\HandlerInterface;

class Handler implements HandlerInterface
{
    private ClientRepository $clientRepository;
    private Flusher $flusher;

    public function __construct(ClientRepository $clientRepository, Flusher $flusher)
    {
        $this->clientRepository = $clientRepository;
        $this->flusher = $flusher;
    }

    /**
     * @param Command $command
     */
    public function handle(CommandInterface $command): void
    {
        $client = Client::create(
            $command->userId
        );

        $this->clientRepository->add($client);
        $client->addEmail($command->email);

        $this->flusher->flush($client);
    }
}

OutputContract

描述

OutputContract 是由 Handler(必要时)形成的 DTO。只包含 Get 方法,其中可以包含有关如何输出字段值的逻辑。可用于生成自动文档。

目的

创建应用程序返回数据的合约,生成自动文档

示例

<?php

declare(strict_types=1);

namespace Path\To\Class;

use App\Model\Profile\Clients\Entity\Client\Client;
use App\Model\Profile\Clients\Entity\Email\Email;
use Bundle\UIBundle\Core\Contract\Command\LocalizationOutputContractInterface;
use DateTimeInterface;
use Symfony\Component\Serializer\Annotation\Ignore;

class CommonOutputContract implements LocalizationOutputContractInterface
{
    /** @Ignore() */
    public Client $client;
    /** @Ignore() */
    private string $locale;

    public function __construct(Client $client, string $locale)
    {
        $this->client = $client;
        $this->locale = $locale;
    }

    public function getId(): string
    {
        return $this->client->getId()->getValue();
    }

    /**
     * @return string[]
     */
    public function getEmails(): array
    {
        return array_map(function (Email $email) {
            return $email->getEmail();
        }, $this->client->getEmails());
    }

    public function getMiddleName(): ?string
    {
        return $this->client->getName()?->getTranslation($this->locale)?->getMiddle();
    }

    public function getLastName(): ?string
    {
        return $this->client->getName()?->getTranslation($this->locale)?->getLast();
    }

    public function getFirstName(): ?string
    {
        return $this->client->getName()?->getTranslation($this->locale)?->getFirst();
    }

    public function getGender(): ?string
    {
        return $this->client->getGender()?->toScalar();
    }

    public function getCreatedAt(): string
    {
        return $this->client->getCreatedAt()->format(DateTimeInterface::ATOM);
    }

    public function getLang(): string
    {
        return $this->locale;
    }
}

内部使用

查询

获取单个

读取操作的示例

use App\Path\To\Entity;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Bundle\UIBundle\Core as UI;

class Controller {
    /**
     * @Route("/{id}.{_format}", methods={"GET"}, name=".read", defaults={"_format"="json"})
     * @OA\Response(
     *     response=200,
     *     description="Read Entity",
     *     @OA\JsonContent(
     *         allOf={
     *             @OA\Schema(ref=@Model(type=UI\Contract\ApiFormatter::class)),
     *             @OA\Schema(type="object",
     *                 @OA\Property(
     *                     property="data",
     *                     type="object",
     *                     @OA\Property(
     *                         property="entity",
     *                         ref=@Model(type=CommonOutputContract::class)
     *                     )
     *                 ),
     *                 @OA\Property(
     *                     property="status",
     *                     example="200"
     *                 )
     *             )
     *         }
     *     )
     * )
     */
    public function read(
        string $id,
        UI\CQRS\Query\GetOne\Processor $processor,
        UI\Dto\OutputFormat $outputFormat,
        UI\Dto\Locale $locale
    ): Response {
        $context = new UI\CQRS\Query\GetOne\Context(
            outputFormat: $outputFormat->getFormat(),
            entityId: $id,
            targetEntityClass: Entity::class,
            outputDtoClass: CommonOutputContract::class,
            locale: $locale
        );
    
        $processor->process($context);
        return $processor->makeResponse();
    }
}

搜索

搜索操作的示例

use App\Path\To\Entity;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Bundle\UIBundle\Core as UI;

class Controller {
    /**
     * @Route(".{_format}", methods={"GET"}, name=".search", defaults={"_format"="json"})
     * @OA\Get(
     *     @OA\Parameter(
     *          name="searchParams",
     *          in="query",
     *          required=false,
     *          @OA\Schema(
     *              ref=@Model(type=UI\Contract\Filter\FilterSortPagination::class)
     *          ),
     *     )
     * )
     * @OA\Response(
     *     response=200,
     *     description="Search by Clients",
     *     @OA\JsonContent(
     *          allOf={
     *              @OA\Schema(ref=@Model(type=UI\Contract\ApiFormatter::class)),
     *              @OA\Schema(type="object",
     *                  @OA\Property(
     *                      property="data",
     *                      type="object",
     *                      @OA\Property(
     *                          property="entities",
     *                          ref=@Model(type=CommonOutputContract::class)
     *                      )
     *                  ),
     *                  @OA\Property(
     *                      property="status",
     *                      example="200"
     *                 )
     *              )
     *          }
     *      )
     * )
     */
    public function search(
        UI\CQRS\Query\Search\Processor $processor,
        UI\Service\Filter\SearchQuery $searchQuery,
        UI\Dto\Locale $locale,
        UI\Dto\OutputFormat $outputFormat
    ): Response {
        $context = new UI\CQRS\Query\Search\Context(
            targetEntityClass: Entity::class,
            outputFormat: $outputFormat->getFormat(),
            outputDtoClass: UseCase\CommonOutputContract::class,
            filterBlackList: ['id'],
            locale: $locale,
            pagination: $searchQuery->getPagination(),
            filters: $searchQuery->getFilters(),
            sorts: $searchQuery->getSorts()
        );
    
        $processor->process($context);
    
        return $processor->makeResponse();
    }
}

命令

Sync(同步命令)

示例

use App\Path\To\UseCase as UseCase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Bundle\UIBundle\Core as UI;

class Controller {
    /**
     * @Route(".{_format}", methods={"POST"}, name=".create", defaults={"_format"="json"})
     * @OA\Post(
     *     @OA\RequestBody(
     *         @OA\MediaType(
     *             mediaType="application/json",
     *             @OA\Schema(
     *                 ref=@Model(type=UseCase\Create\Contract::class)
     *             )
     *         )
     *     )
     * )
     * @OA\Response(
     *     response=200,
     *     description="Create User",
     *     @OA\JsonContent(
     *          allOf={
     *              @OA\Schema(ref=@Model(type=UI\Contract\ApiFormatter::class)),
     *              @OA\Schema(type="object",
     *                  @OA\Property(
     *                      property="data",
     *                      type="object",
     *                      @OA\Property(
     *                          property="entities",
     *                          ref=@Model(type=UseCase\CommonOutputContract::class)
     *                      )
     *                  ),
     *                  @OA\Property(
     *                      property="status",
     *                      example="200"
     *                 )
     *              )
     *          }
     *      )
     * )
     */
    public function create(
        UI\CQRS\Command\Sync\Processor $processor,
        UI\Dto\OutputFormat $outputFormat,
        UseCase\Create\Contract $contract,
        UseCase\Create\Handler $handler
    ): Response {
        $command = new UseCase\Create\Command();
        $command->mapContract($contract);
    
        $context = new UI\CQRS\Command\Sync\Context(
            handler: $handler,
            command: $command,
            outputFormat: $outputFormat->getFormat(),
        );
    
        $processor->process($context);
        return $processor->makeResponse();
    }
}

Async(异步命令)

示例

use App\Path\To\Entity;
use App\Path\To\UseCase as UseCase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;
use Bundle\UIBundle\Core as UI;

class Controller {
    /**
     * @Route(".{_format}", methods={"POST"}, name=".create", defaults={"_format"="json"})
     * @OA\Post(
     *     @OA\RequestBody(
     *         @OA\MediaType(
     *             mediaType="application/json",
     *             @OA\Schema(
     *                 ref=@Model(type=UseCase\Create\Contract::class)
     *             )
     *         )
     *     )
     * )
     * @OA\Response(
     *     response=200,
     *     description="Create Message",
     *     @OA\JsonContent(
     *          allOf={
     *              @OA\Schema(ref=@Model(type=UI\Contract\ApiFormatter::class)),
     *              @OA\Schema(type="object",
     *                  @OA\Property(
     *                      property="ok",
     *                      example=true
     *                 )
     *                  @OA\Property(
     *                      property="status",
     *                      example="200"
     *                 )
     *              )
     *          }
     *      )
     * )
     */
    #[Route(".{_format}", name: '.create', defaults: ['_format' => 'json'], methods: ["POST"])]
    public function create(
        UI\CQRS\Command\Async\Processor $processor,
        UI\Dto\OutputFormat $outputFormat,
        UseCase\Create\Contract $contract
    ): Response {
        $command = new UseCase\Create\Command();
        $command->mapContract($contract);
    
        $context = new UI\CQRS\Command\Async\Context(
            command: $command,
            outputFormat: $outputFormat->getFormat(),
        );
    
        $processor->process($context);
        return $processor->makeResponse();
    }
}