3

在官方 Api-Platform 网站上有一个通用设计注意事项页面。

最后但同样重要的是,要创建基于事件溯源的系统,一种方便的方法是:

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

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

因此,我尝试通过一种简化来实现这种方法(使用一个 DB,但读取和写入是分开的)。

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

我创建了一个UserDoctrine 实体和我想用@Serializer\Groups({"Read"}). 我将在此处省略它,因为它非常通用。

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

# 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')"

因此,如上所示,UserDoctrine 实体是只读的,因为只GET定义了方法。

然后我创建了一个CreateUserDTO:

# 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;
}

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

# 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处理. 在我使用 Doctrine 实体来写入数据,但对于这种情况,这并不重要,因为 Api 平台不知道 DataPersister 将如何写入它。因此,从概念上讲 - 它是读取和写入的分离。POST/usersCreateUserDataPersister::persist

读取由 DoctrineDataProvider附带的 Api 平台执行,写入由 custom 执行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": "test@custom.domain",
  "plainPassword": "123qweQWE"
}

问题! 我收到400回复... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...

但是,一条新记录被添加到数据库中,因此自定义 DataPersister 有效;)

根据一般设计考虑,实现了写入和读取的分离,但没有按预期工作。

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

很乐意得到任何帮助!

更新 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:

数据持久化

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

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

4

3 回答 3

3

简答

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

长答案

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

因此,为了在实体上生成 IRI,它需要定义一个 GET 项目操作,因为那是资源 IRI 的路由。

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

修复步骤

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

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

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

Controller Result 应该是您的 DTO 的一个实例,Persist Result 应该是您的 User 实体的一个实例,但我猜它正在返回您的 DTO。

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

希望这会有所帮助!

于 2018-08-21T21:52:56.123 回答
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": "test@custom.domain",
  "plainPassword": "123qweQWE"
}

PS:对不起我的英语:)

于 2018-09-05T13:06:50.543 回答
0

仅适用于 2.4 版本,但确实很有帮助。

只需 output_class=false为 CreateUserDTO 添加,POST|PUT|PATCH 一切都会好起来的

output_classfalse允许您绕过获取项目操作。您可以在ApiPlatform\Core\EventListener#L68中看到。

于 2019-02-12T17:01:02.340 回答