Published on

Api platform 教程7---State Processors和State Providers

原创文章,转载时需取得本人同意并注明来源
Authors
  • Api platform 教程7---State Processors和State Providers
    Name
    langziyang
    Twitter

Api platform 教程7---State Processors和State Providers

在api platform中,向API提交数据时,有一个Processor,可以在POST,PUT,PATCH和DELETE时,对数据进行处理。 比如上节我们提到的对密码进行加密。我们可以通过命令来创建一个processor

php bin/console make:state-processor

默认会在src/State目录存放该文件,但可能因为我们的文件会很多,管理不方便,所以我在开发过程中喜欢根据Entity名称来存放

# src/State/User/Processor/UserProcessor.php
<?php

namespace App\State\User\Processor;

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserProcessor implements ProcessorInterface
{
    public function __construct(
        #[Autowire(service: PersistProcessor::class)]
        private ProcessorInterface          $processor,
        private UserPasswordHasherInterface $hasher
    )
    {
    }

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
    {
        assert($data instanceof User);
        $data->setPassword($this->hasher->hashPassword($data, $data->getPassword()));
        $this->processor->process($data, $operation, $uriVariables, $context);
        return $data;
    }
}
# src/Entity/User.php
#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(processor: UserProcessor::class),
        new Patch(processor: UserProcessor::class),
        new Delete(),
    ]
)]

因为我们在post或者patch时都有可能加密密码,所以分别在两个操作上都加上了processor,如果整个Entity中每一个提交过程都需要加密, 比如说上面代码中的Delete也需要的话,你可以写在外面

#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(),
        new Patch(),
        new Delete(),
    ],
    processor:  UserProcessor::class
)]

提交数据后,我们可以看到,返回数据中的密码已经被加密了,但是代码中的逻辑判断需要你自己处理:比如说在修改用户信息时可能不会提供密码。

对于processor我们已经了解了,接下来我们看看provider。在Get和GetCollection操作时,有一个provider过程,在这里进行的操作会返回给我们 所有想要的数据。

# src/State/User/Provider/UserProvider.php
<?php

namespace App\State\User\Provider;

use ApiPlatform\Doctrine\Orm\State\CollectionProvider;
use ApiPlatform\Doctrine\Orm\State\ItemProvider;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class UserProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: CollectionProvider::class)]
        private ProviderInterface $collectionProvider,
        #[Autowire(service: ItemProvider::class)]
        private ProviderInterface $itemProvider,
    )
    {
    }

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if ($operation instanceof CollectionOperationInterface) {
            return $this->collectionProvider->provide($operation, $uriVariables, $context);
        }
        $user = $this->itemProvider->provide($operation, $uriVariables, $context);
        assert($user instanceof User);
        $user->setEmail('123@qq.com');
        return $user;
    }
}
#src/Entity/User.php
#[ApiResource(
    operations: [
        new GetCollection(provider: UserProvider::class),
        new Get(provider: UserProvider::class),
        new Post(processor: UserProcessor::class),
        new Patch(processor: UserProcessor::class),
        new Delete(),
    ]
)]

可以看到,我们在GetCollection和Get中写上了provider,当前,你也可以只在需要的操作中提供。

然后通过$operation判断如果是collection,就返回原来的数据,并没有对数据进行任何修改。

在get中,我们查找出对应的User,然后修改了一下email。但是现在的问题是:如果我希望无论是collection还是get,都去修改用户 的字段或者说再多添加一个返回字段呢?当然,你可以在collection中对取出来的数据进行foreach进行修改。如果想添加数据的话还需要去 Entity文件中添加字段。这不是一个好办法

其实Api platform在返回jsonld数据时是使用的symfony的序列化过程,所以我们可以写一个对应的过程

php bin/console make:serializer:normalizer
<?php

namespace App\Serializer\Normalizer;

use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class UserNormalizer implements NormalizerInterface
{
    private const USER_ALREADY_NORMALIZED = 'user_already_normalized';

    public function __construct(
        #[Autowire(service: 'serializer.normalizer.object')]
        private NormalizerInterface $normalizer
    )
    {
    }

    public function normalize($object, ?string $format = null, array $context = []): array
    {
        $context[self::USER_ALREADY_NORMALIZED] = true;
        $data = $this->normalizer->normalize($object, $format, $context);
        $data['custom']='custom_normalized';
        return $data;
    }

    public function supportsNormalization($data, ?string $format = null, array $context = []): bool
    {
        return $data instanceof User && !isset($context[self::USER_ALREADY_NORMALIZED]);
    }

    public function getSupportedTypes(?string $format): array
    {
        return [User::class => true];
    }
}