intellect-web-development / symfony-presentation-bundle
用于处理命令和查询的工具包(包括带有过滤、排序和分页的搜索引擎)
0.4.1
2023-08-04 17:10 UTC
Requires
- php: >=8.1
- ext-mbstring: *
Requires (Dev)
- doctrine/orm: ^2.13
- friendsofphp/php-cs-fixer: ^3.14
- overtrue/phplint: ^3.0
- phpmetrics/phpmetrics: ^2.7
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.5
- psalm/plugin-symfony: ^4.0
- roave/security-advisories: dev-latest
- symfony/config: ^6.1
- symfony/dependency-injection: ^6.1
- symfony/http-kernel: ^6.1
- symfony/security-bundle: ^6.1
- symfony/serializer: ^6.1
- symfony/validator: ^6.1
- vimeo/psalm: ^4.6
- zircote/swagger-php: ^3.2
README
描述
本包旨在方便地处理symfony应用程序的表现层。
用途
- 验证API方法的输入参数
- 基于OpenApi兼容的DTO生成Swagger文档
- 对 doctrine 实体进行排序、过滤和分页
- 根据 doctrine 实体获取资源/聚合体
过滤示例
查询示例
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'
过滤
描述
搜索运算符
示例
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'
代码示例
查询
定义
查询 - 获取实体(资源/聚合体)当前状态的请求,不更改其实体状态。
聚合
获取聚合数据的请求。
读取操作的示例
<?php declare(strict_types=1); namespace App\Http\User\Read; use App\Entity\User; use App\Http\User\CommonOutputContract; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Security; use OpenApi\Annotations as OA; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use IWD\Symfony\PresentationBundle\Dto\Input\OutputFormat; use IWD\Symfony\PresentationBundle\Dto\Output\ApiFormatter; use IWD\Symfony\PresentationBundle\Service\Presenter; use IWD\Symfony\PresentationBundle\Service\QueryBus\Aggregate\Bus; use IWD\Symfony\PresentationBundle\Service\QueryBus\Aggregate\Query; class Action { /** * @OA\Tag(name="User") * @OA\Response( * response=200, * description="Read User", * @OA\JsonContent( * allOf={ * @OA\Schema(ref=@Model(type=ApiFormatter::class)), * @OA\Schema(type="object", * @OA\Property( * property="data", * ref=@Model(type=CommonOutputContract::class) * ), * @OA\Property( * property="status", * example="200" * ) * ) * } * ) * ) * @OA\Response( * response=400, * description="Bad Request" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * @OA\Response( * response=404, * description="Resource Not Found" * ) * @Security(name="Bearer") */ #[Route( data: '/users/{id}.{_format}', name: 'users.read', defaults: ['_format' => 'json'], methods: ['GET'] )] public function read(string $id, Bus $bus, OutputFormat $outputFormat, Presenter $presenter): Response { $query = new Query( aggregateId: $id, targetEntityClass: User::class ); /** @var User $user */ $user = $bus->query($query); return $presenter->present( data: ApiFormatter::prepare( CommonOutputContract::create($user) ), outputFormat: $outputFormat ); } }
搜索操作的示例
<?php declare(strict_types=1); namespace App\Http\User\Search; use App\Entity\User; use App\Http\User\CommonOutputContract; use IWD\Symfony\PresentationBundle\Service\Presenter; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Security; use OpenApi\Annotations as OA; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use IWD\Symfony\PresentationBundle\Dto\Input\OutputFormat; use IWD\Symfony\PresentationBundle\Dto\Input\SearchQuery; use IWD\Symfony\PresentationBundle\Dto\Output\ApiFormatter; use IWD\Symfony\PresentationBundle\Dto\Output\OutputPagination; use IWD\Symfony\PresentationBundle\Service\QueryBus\Search\Bus; use IWD\Symfony\PresentationBundle\Service\QueryBus\Search\Query; class Action { /** * @OA\Tag(name="User") * @OA\Get( * @OA\Parameter( * name="searchQuery", * in="query", * required=false, * @OA\Schema( * ref=@Model(type=QueryParams::class) * ), * ) * ) * @OA\Response( * response=200, * description="Search by Users", * @OA\JsonContent( * allOf={ * @OA\Schema(ref=@Model(type=ApiFormatter::class)), * @OA\Schema( * type="object", * @OA\Property( * property="data", * type="object", * @OA\Property( * property="data", * ref=@Model(type=CommonOutputContract::class), * type="object" * ), * @OA\Property( * property="pagination", * ref=@Model(type=OutputPagination::class), * type="object" * ) * ), * @OA\Property( * property="status", * example="200" * ) * ) * } * ) * ) * @OA\Response( * response=400, * description="Bad Request" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * @OA\Response( * response=404, * description="Resource Not Found" * ) * @Security(name="Bearer") */ #[Route( data: '/users.{_format}', name: 'users.search', defaults: ['_format' => 'json'], methods: ['GET'] )] public function search( Bus $bus, SearchQuery $searchQuery, OutputFormat $outputFormat, Presenter $presenter ): Response { $query = new Query( targetEntityClass: User::class, pagination: $searchQuery->pagination, filters: $searchQuery->filters, sorts: $searchQuery->sorts ); $searchResult = $bus->query($query); return $presenter->present( data: ApiFormatter::prepare([ 'data' => array_map(static function (User $user) { return CommonOutputContract::create($user); }, $searchResult->entities), 'pagination' => $searchResult->pagination ]), outputFormat: $outputFormat ); } }
SearchQueryParams 示例
<?php declare(strict_types=1); namespace App\Http\User\Search; use OpenApi\Annotations as OA; use IWD\Symfony\PresentationBundle\Dto\Input\Filters; use IWD\Symfony\PresentationBundle\Dto\Input\SearchQuery; class QueryParams extends SearchQuery { /** * @OA\Property( * property="filter", * type="object", * example={ * "id": {"eq": "ab4ac777-e054-45ec-b997-b69062917d10"}, * "createdAt": {"range": "2022-02-22 12:00:00,2022-02-22 14:00:00"}, * "updatedAt": {"range": "2022-02-22 12:00:00,2022-02-22 14:00:00"}, * "email": {"eq": "user@dev.ru"}, * "status": {"eq": "active"} * } * ) */ public Filters $filters; }
命令操作的示例
<?php declare(strict_types=1); namespace App\Http\User\Create; use App\Http\User\CommonOutputContract; use App\Entity\User\UseCase\Create\Handler; use IWD\Symfony\PresentationBundle\Service\Presenter; use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Security; use OpenApi\Annotations as OA; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use IWD\Symfony\PresentationBundle\Dto\Input\OutputFormat; use IWD\Symfony\PresentationBundle\Dto\Output\ApiFormatter; class Action { /** * @OA\Tag(name="Auth.User") * @OA\Post( * @OA\RequestBody( * @OA\MediaType( * mediaType="application/json", * @OA\Schema( * ref=@Model(type=InputContract::class) * ) * ) * ) * ) * @OA\Response( * response=200, * description="Create User", * @OA\JsonContent( * allOf={ * @OA\Schema(ref=@Model(type=ApiFormatter::class)), * @OA\Schema(type="object", * @OA\Property( * property="data", * ref=@Model(type=CommonOutputContract::class) * ), * @OA\Property( * property="status", * example="200" * ) * ) * } * ) * ) * @OA\Response( * response=400, * description="Bad Request" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ), * @OA\Response( * response=404, * description="Resource Not Found" * ) * @Security(name="Bearer") */ #[Route( data: '/users/create.{_format}', name: 'users.create', defaults: ['_format' => 'json'], methods: ['POST'] )] public function create( OutputFormat $outputFormat, InputContract $contract, Handler $handler, Presenter $presenter ): Response { $user = $handler->handle( $contract->createCommand() ); return $presenter->present( data: ApiFormatter::prepare( data: CommonOutputContract::create($user), messages: ['User created'] ), outputFormat: $outputFormat ); } }
InputContract 示例
<?php declare(strict_types=1); namespace App\Http\User\Create; use App\Entity\User\ValueObject\Email as UserEmail; use App\Entity\User\UseCase\Create\Command; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotNull; use IWD\Symfony\PresentationBundle\Interfaces\InputContractInterface; class InputContract implements InputContractInterface { #[NotNull] #[Length(min: 3, max: 255)] public string $password; #[NotNull] #[Email] #[Length(max: 255)] public string $email; public function createCommand(): Command { return new Command( password: $this->password, email: new UserEmail($this->email) ); } }
OutputContract 示例
<?php declare(strict_types=1); namespace App\Http\Contract\User; use App\Entity\User; use DateTimeInterface; class CommonOutputContract { public string $id; public string $createdAt; public string $updatedAt; public string $email; public string $status; public string $role; public static function create(User $user): self { $contract = new self(); $contract->id = $user->getId()->getValue(); $contract->createdAt = $user->getCreatedAt()->format(DateTimeInterface::ATOM); $contract->updatedAt = $user->getUpdatedAt()->format(DateTimeInterface::ATOM); $contract->email = $user->getEmail()->getValue(); $contract->status = $user->getStatus(); $contract->role = $user->getRole()->getName(); return $contract; } }