lshamanl / symfony-ui-bundle
Symfony API-Adapter, UI-Bundle, Filters
Requires
- php: >=8.0
- ext-mbstring: *
- composer/package-versions-deprecated: ^1.11
- doctrine/annotations: ^1.13
- doctrine/doctrine-bundle: ^2.4
- doctrine/doctrine-migrations-bundle: ^3.1
- doctrine/orm: ^2.9
- symfony/config: ^5.2
- symfony/dependency-injection: ^5.2
- symfony/serializer: ^5.2
- symfony/translation: ^5.2
- symfony/validator: ^5.2
- zircote/swagger-php: ^3.2
Requires (Dev)
- fakerphp/faker: 1.13.0
- overtrue/phplint: ^3.0
- phpmetrics/phpmetrics: ^2.7
- phpstan/phpstan: ^0.12.81
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-master
- squizlabs/php_codesniffer: ^3.5
- vimeo/psalm: ^4.6
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(); } }