只为管理员创建一个字段@Assert\NotBlank

Make a field @Assert\NotBlank only for admin

第一次发帖,如有不妥之处,敬请见谅。

我在我的 Symfony 5.3 项目中使用 API 平台。我正在尝试使用某些规则在我的一个实体中创建一个可写字段。该实体称为 StripeAccount,必须链接到 $company 对象(参见下面的映射)。这是规则

这是我的 StripeAccount 实体:

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\StripeAccountRepository;
use App\Validator\IsValidCompany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @Vich\Uploadable
 * @ApiResource(
 *      iri="http://schema.org/StripeAccount",
 *      normalizationContext={"groups"={"read:StripeAccount"}, "enable_max_depth"=true},
 *      denormalizationContext={"groups"={"write:StripeAccount"}},
 *      collectionOperations={
 *          "post"={
 *              "input_formats"={
 *                  "multipart"={"multipart/form-data"}
 *              },
 *          },
 *      },
 *      itemOperations={
 *          "get"
 *      }
 * )
 * @ORM\Entity(repositoryClass=StripeAccountRepository::class)
 */
class StripeAccount
{
    public const ACCOUNT_TYPE = 'custom';

    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"read:StripeAccount", "write:StripeAccount"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Company::class, inversedBy="stripeAccounts")
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"read:StripeAccount", "admin:write"})
     * @Assert\NotBlank(groups={"admin:write"})
     * @IsValidCompany
     */
    private $company;

    /**
     * @ORM\OneToMany(targetEntity=Brand::class, mappedBy="stripeAccount")
     * @Groups({"read:StripeAccount", "write:StripeAccount"})
     */
    private $brands;

    // other fields


    public function __construct()
    {
        $this->brands = new ArrayCollection();
    }

    public static function getType(): string
    {
        return self::ACCOUNT_TYPE;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getCompany(): ?Company
    {
        return $this->company;
    }

    public function setCompany(?Company $company): self
    {
        $this->company = $company;
        return $this;
    }

    // other methods
}

我遵循了这个教程:https://symfonycasts.com/screencast/api-platform-security/context-builder#play(第 25 章和第 33 到 36 章),所以我有了这个验证器:

<?php

namespace App\Validator;

use App\Entity\{Company, User};
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class IsValidCompanyValidator extends ConstraintValidator
{
    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    public function validate($value, Constraint $constraint)
    {
        /* @var $constraint \App\Validator\IsValidCompany */

        if (null === $value || '' === $value) {
            return;
        }

        $user = $this->security->getUser();
        if (!$user instanceof User) {
            $this->context->buildViolation($constraint->anonymousMessage)->addViolation();
            return;
        }

        if ($this->security->isGranted('ROLE_ADMIN')) {
            return;
        }

        if (!$value instanceof Company) {
            throw new \InvalidArgumentException(
                '@IsValidCompany constraint must be put on a property containing a Company object'
            );
        }

        if ($value->getId() !== $user->getId()) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('%value%', $value)
                ->addViolation();
        }
    }
}

和这个 ContextBuilder :

<?php

namespace App\Serializer;

use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class AdminGroupsContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

    public function __construct(
        SerializerContextBuilderInterface $decorated,
        AuthorizationCheckerInterface $authorizationChecker
    ) {
        $this->decorated = $decorated;
        $this->authorizationChecker = $authorizationChecker;
    }

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

        $isAdmin = $this->authorizationChecker->isGranted('ROLE_ADMIN');

        if (isset($context['groups']) && $isAdmin) {
            $context['groups'][] = $normalization ? 'admin:read' : 'admin:write';
        }

        return $context;
    }
}

一切正常,如果发出请求的用户是管理员,则添加组 'admin:write',如果用户不是管理员,则设置 $company。

我的问题是: 我的@Assert\NotBlank(groups={"admin:write"}) 被完全忽略了。我尝试使用 @Groups 注释甚至 denormalizationContext 进行一些调整,但没有,它在任何时候都没有应用。我在这里错过了什么?

顺便说一句,我正在使用 Postman 来测试我的 API

非常感谢您的帮助

[编辑] 根据 @TekPike 的回答,这是我的工作代码:

StripeAccount.php

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\StripeAccountRepository;
use App\Validation\AdminValidationGroupsGenerator;
use App\Validator\IsValidCompany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ApiResource(
 *      iri="http://schema.org/StripeAccount",
 *      attributes={
 *          "validation_groups"=AdminValidationGroupsGenerator::class,
 *      },
 *      normalizationContext={"groups"={"read:StripeAccount"}, "enable_max_depth"=true},
 *      denormalizationContext={"groups"={"write:StripeAccount"}},
 *      collectionOperations={
 *          "post"={
 *              "input_formats"={
 *                  "multipart"={"multipart/form-data"}
 *              },
 *          },
 *      },
 *      itemOperations={
 *          "get",
 *          "delete",
 *      }
 * )
 * @ORM\Entity(repositoryClass=StripeAccountRepository::class)
 */
class StripeAccount
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"read:StripeAccount", "write:StripeAccount"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Company::class, inversedBy="stripeAccounts")
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"read:StripeAccount", "admin:write"})
     * @Assert\NotBlank(groups={"admin:write"})
     * @IsValidCompany
     */
    private $company;


    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"read:StripeAccount", "write:StripeAccount"})
     * @Assert\NotBlank
     */
    private $name;

    // ...
}

还有我的 AdminValidationGroupsGenerator.php :

<?php

namespace App\Validation;

use ApiPlatform\Core\Bridge\Symfony\Validator\ValidationGroupsGeneratorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class AdminValidationGroupsGenerator implements ValidationGroupsGeneratorInterface
{
    private $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    /**
     * {@inheritdoc}
     */
    public function __invoke($entity): array
    {
        $reflect = new \ReflectionClass($entity);
        $name = "write:" . $reflect->getShortName();
        return $this->authorizationChecker->isGranted('ROLE_ADMIN', $entity) ? [$name, 'admin:write'] : [$name];
    }
}

您混淆了序列化组验证组

目前您使用注释 denormalizationContext={"groups"={"write:StripeAccount"}} 和 class App\SerializerAdminGroupsContextBuilder.

定义序列化组

但是,约束 @Assert\NotBlank(groups={"admin:write"}) 中定义的“admin:write”组是验证组。

在您的情况下,由于验证组会根据用户而变化,因此您必须使用 dynamic validation groups