在api-platform中实现事件采购/ CQRS方法

问题描述 投票:2回答:3

在官方Api-Platform网站上有一个General Design Considerations页面。

最后但并非最不重要的是,要创建基于事件采购的系统,一种方便的方法是:

  • 使用自定义数据容器在事件存储中保留数据
  • 在标准RDBMS(Postgres,MariaDB ...)表或视图中创建投影
  • 使用只读Doctrine实体类映射这些投影,并使用@ApiResource标记这些类

然后,您可以从API平台提供的内置Doctrine过滤器,排序,分页,自动连接等中受益。

因此,我试图通过一种简化来实现这种方法(使用一个DB,但具有独立的读写)。

但是失败了......有一个问题,我不知道如何解决,所以请你帮忙!

我创建了一个User Doctrine实体和我希望用@Serializer\Groups({"Read"})公开的带注释的字段。我会在这里省略它,因为它非常通用。

适用于api平台的yaml格式的User资源:

# config/api_platform/entities/user.yaml

App\Entity\User\User:
    attributes:
        normalization_context:
            groups: ["Read"]
    itemOperations:
        get: ~
    collectionOperations:
        get:
            access_control: "is_granted('ROLE_ADMIN')"

所以,正如上面所示,User Doctrine实体是只读的,因为只定义了GET方法。

然后我创建了一个CreateUser DTO:

# src/Dto/User/CreateUser.php

namespace App\Dto\User;

use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;

final class CreateUser
{
    /**
     * @var string
     * @Assert\NotBlank()
     * @Assert\Email()
     * @AppAssert\FakeEmailChecker()
     */
    public $email;
    /**
     * @var string
     * @Assert\NotBlank()
     * @AppAssert\PlainPassword()
     */
    public $plainPassword;
}

适用于api平台的yaml格式的CreateUser资源:

# config/api_platform/dtos/create_user.yaml

App\Dto\User\CreateUser:
    itemOperations: {}
    collectionOperations:
        post:
            access_control: "is_anonymous()"
            path: "/users"
            swagger_context:
                tags: ["User"]
                summary: "Create new User resource"

因此,在这里您可以看到只定义了一个POST方法,完全用于创建新用户。

这里路由器显示:

$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name                               Method   Scheme   Host   Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection   POST     ANY      ANY    /users
api_users_get_collection           GET      ANY      ANY    /users.{_format}
api_users_get_item                 GET      ANY      ANY    /users/{id}.{_format}

我还添加了一个自定义DataPersister来处理POST/users。在CreateUserDataPersister::persist中,我使用Doctrine实体来编写数据,但是对于这种情况并不重要,因为Api-platform对DataPersister将如何编写它一无所知。因此,从概念来看 - 它是读写分离。

读取由Doctrine的ap-platform附带的DataProvider执行,写入由自定义DataPersister执行。

# src/DataPersister/CreateUserDataPersister.php

namespace App\DataPersister;

use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;

class CreateUserDataPersister implements DataPersisterInterface
{
    private $manager;

    public function __construct(EntityManagerInterface $manager)
    {
        $this->manager = $manager;
    }

    public function supports($data): bool
    {
        return $data instanceof CreateUser;
    }

    public function persist($data)
    {
        $user = new User();
        $user
            ->setEmail($data->email)
            ->setPlainPassword($data->plainPassword);

        $this->manager->persist($user);
        $this->flush();

        return $user;
    }

    public function remove($data)
    {

    }
}

当我执行创建新用户的请求时:

POST https://{{host}}/users
Content-Type: application/json

{
  "email": "[email protected]",
  "plainPassword": "123qweQWE"
}

问题!我得到了400响应... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...

但是,新记录被添加到数据库,因此自定义DataPersister工作;)

根据General Design Considerations,写入和读取的分离已实现,但未按预期工作。

我很确定,我可能会遗漏一些配置或实现的东西。所以,这就是为什么它不起作用。

很乐意得到任何帮助!

更新1:

问题出在\ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName()。在第48-59行,它遍历试图找到适当路线的所有路线:

  • $operationType = 'item'
  • $resourceClass = 'App\Dto\User\CreateUser'

$operationType = 'item'仅针对$resourceClass = 'App\Entity\User\User'定义,因此无法找到路线并引发异常。

更新2:

所以,问题可能听起来像这样:

如何使用Doctrine实体进行读取和DTO进行写入来实现读写分离(CQS?),两者都驻留在同一路径上,但使用不同的方法?

更新3:

Data Persisters

  • 将数据存储到其他持久层(ElasticSearch,MongoDB,外部Web服务......)
  • 不公开通过API公开与数据库映射的内部模型
  • 通过实现CQRS等模式,使用单独的模型进行读取操作和更新

是!我想要那个......但是如何在我的例子中实现它?

cqrs symfony4 event-sourcing api-platform.com command-query-separation
3个回答
1
投票

简答

问题是Dto \ User \ CreateUser对象正在为响应进行序列化,实际上,您实际上希望返回并序列化Entity \ User。

答案很长

当API平台序列化资源时,它们将为资源生成IRI。 IRI代是代码所在的地方。默认的IRI生成器使用Symfony路由器根据API平台创建的API路由实际构建路由。

因此,为了在实体上生成IRI,需要定义GET项操作,因为这是将成为资源的IRI的路由。

在您的情况下,DTO没有GET项操作(并且不应该有),但是当API平台尝试序列化您的DTO时,它会抛出该错误。

修复的步骤

从您的代码示例中,看起来似乎正在返回User,但是,从错误中可以清楚地看出User实体不是被序列化的实体。

要做的一件事是安装debug-pack,使用bin/console server:dump启动转储服务器,并在API平台WriteListener中添加一些转储语句:第53行附近的ApiPlatform \ Core \ EventListener \ WriteListener:

dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);

控制器结果应该是您的DTO的实例,持久结果应该是您的用户实体的实例,但我猜它正在返回您的DTO。

如果它返回你的DTO,你需要调试并找出为什么从dataPersister-> persist而不是User实体返回DTO。也许您的系统中存在可能导致冲突的其他数据持久性或事物。

希望这有帮助!


0
投票

您需要在答案中发送“id”。

如果User是Doctrine实体,请使用:

/**
 * @ORM\Id()
 * @ORM\GeneratedValue()
 * @ORM\Column(type="integer")
 */
private $id;

如果User不是Doctrine实体,请使用:

/**
 * @Assert\Type(type="integer")
 * @ApiProperty(identifier=true)
 */
private $id;

无论如何,你的答案是这样的:

{
  "id": 1, // Your unique id of User
  "email": "[email protected]",
  "plainPassword": "123qweQWE"
}

P.S。:对不起我的英文:)


0
投票

仅适用于2.4版本,但真的很有帮助。

只需为CreateUserDTO添加output_class=false,一切都可以用于POST | PUT | PATCH

output_class为false允许您绕过get项操作。你可以在ApiPlatform \ Core \ EventListener#L68中看到它。

© www.soinside.com 2019 - 2024. All rights reserved.