You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

API Platform中如何根据used字段控制ResetPasswordHashes的contact字段返回?

解决ResetPasswordHashes实体的两个API配置需求

针对你提出的「仅返回$used为false的实体记录」和「当$used为false时保护$contact信息」这两个需求,我整理了以下符合API Platform最佳实践的实现方案:


1. 强制仅返回$used=false的实体记录

要确保所有针对该实体的API请求(包括单条查询和列表查询)都只返回未使用的哈希记录,最彻底的方式是使用Doctrine Query Extension,它会自动给所有查询添加过滤条件,避免遗漏:

首先创建自定义的Query扩展类:

// src/Doctrine/ResetPasswordHashesQueryExtension.php
namespace App\Doctrine;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\ResetPasswordHashes;
use Doctrine\ORM\QueryBuilder;

final class ResetPasswordHashesQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
    {
        $this->addUsedFilter($queryBuilder, $resourceClass);
    }

    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
    {
        $this->addUsedFilter($queryBuilder, $resourceClass);
    }

    private function addUsedFilter(QueryBuilder $queryBuilder, string $resourceClass): void
    {
        if (ResetPasswordHashes::class !== $resourceClass) {
            return;
        }

        $rootAlias = $queryBuilder->getRootAliases()[0];
        $queryBuilder->andWhere(sprintf('%s.used = :isUnused', $rootAlias))
            ->setParameter('isUnused', false);
    }
}

这个类会自动拦截所有针对ResetPasswordHashes的查询,添加used=false的条件,不管是列表请求还是单条详情请求,都只会返回未使用的记录。

如果你只想在特定操作上生效,也可以直接在ApiResource的操作配置中指定默认过滤参数:

#[ApiResource(
    operations: [
        new Get(
            normalizationContext: ['groups' => ['read']],
            filters: ['reset_password_hashes.used_filter' => ['used' => false]]
        ),
        new GetCollection(
            normalizationContext: ['groups' => ['read']],
            filters: ['reset_password_hashes.used_filter' => ['used' => false]]
        ),
    ],
    normalizationContext: ['groups' => ['read']],
)]

不过Query Extension的方式更可靠,能确保所有请求都遵守过滤规则,不会因为忘记配置某个操作而出现数据泄露。


2. 当$used=false时隐藏$contact字段

要实现动态控制字段的序列化,我们可以通过自定义序列化上下文构建器来根据实体的used值调整序列化分组或直接排除字段:

方案一:使用分组切换(更灵活)

首先创建自定义的Serializer Context Builder:

// src/Serializer/ResetPasswordHashesContextBuilder.php
namespace App\Serializer;

use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use App\Entity\ResetPasswordHashes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

final class ResetPasswordHashesContextBuilder implements SerializerContextBuilderInterface
{
    public function __construct(private readonly SerializerContextBuilderInterface $decorated) {}

    public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array
    {
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);

        $entity = $context[AbstractNormalizer::OBJECT_TO_POPULATE] ?? null;
        if (!$entity instanceof ResetPasswordHashes || !$normalization) {
            return $context;
        }

        // 当used为false时,切换到不含contact的分组
        if (!$entity->used) {
            $context['groups'] = array_filter($context['groups'], fn($group) => $group !== 'read');
            $context['groups'][] = 'read_without_contact';
        }

        return $context;
    }
}

然后调整实体字段的分组配置,把$contact只加入read分组,其他字段同时加入read和read_without_contact:

// ResetPasswordHashes.php
#[ORM\Id, ORM\Column(name: "id"), ORM\GeneratedValue]
#[Groups(['read', 'read_without_contact'])]
public int $id;

#[ORM\Column(name: "hash", length: 255)]
#[Groups(['read', 'read_without_contact'])]
public string $hash;

#[ORM\Column(name: 'used')]
#[Groups(['read', 'read_without_contact'])]
public bool $used;

#[ORM\OneToOne(targetEntity: Contact::class)]
#[ORM\JoinColumn(name: 'contact_id', referencedColumnName: 'contact_id')]
#[Groups(['read'])] // 仅在read分组中包含contact
public ?Contact $contact;

#[ORM\Column(name: "created_at")]
#[Groups(['read', 'read_without_contact'])]
public ?\DateTime $createdAt;

方案二:直接排除字段(更简洁)

如果你不想新增分组,也可以直接在Context Builder中排除contact字段:

修改Context Builder的逻辑:

if (!$entity->used) {
    $context['attributes'] = ['exclude' => ['contact']];
}

这种方式不需要修改实体的分组配置,直接在序列化时动态排除字段,适合快速实现。


注意:Symfony 5+版本会自动注册这些自定义服务,如果你使用的是更老的版本,需要手动在config/services.yaml中添加服务配置。

内容的提问来源于stack exchange,提问作者Richard Bridge

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 16:30:36