只为管理员创建一个字段@Assert\NotBlank
Make a field @Assert\NotBlank only for admin
第一次发帖,如有不妥之处,敬请见谅。
我在我的 Symfony 5.3 项目中使用 API 平台。我正在尝试使用某些规则在我的一个实体中创建一个可写字段。该实体称为 StripeAccount,必须链接到 $company 对象(参见下面的映射)。这是规则
- 如果用户未被授予 ROLE_ADMIN,则 $company 不是必需的,因为它将自动填写
- 如果用户未被授予 ROLE_ADMIN 并提供 $company,则它必须与用户的 $company 匹配(否则将添加违规)
- 如果授予用户 ROLE_ADMIN,则 $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。
第一次发帖,如有不妥之处,敬请见谅。
我在我的 Symfony 5.3 项目中使用 API 平台。我正在尝试使用某些规则在我的一个实体中创建一个可写字段。该实体称为 StripeAccount,必须链接到 $company 对象(参见下面的映射)。这是规则
- 如果用户未被授予 ROLE_ADMIN,则 $company 不是必需的,因为它将自动填写
- 如果用户未被授予 ROLE_ADMIN 并提供 $company,则它必须与用户的 $company 匹配(否则将添加违规)
- 如果授予用户 ROLE_ADMIN,则 $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。