这是我第一次发帖,如果我没有遵守一些规则,我很抱歉。
我在Symfony 5.3项目中使用API平台。我想让我的一个实体中的一个字段用一些规则可写。该实体名为StripeAccount,必须链接到$company对象(参见下面的映射)。以下是规则
- 如果用户没有被授予ROLE_ADMIN,那么$company不是强制性的,因为它将自动填充
- 如果用户没有被授予ROLE_ADMIN并且提供$company,它必须匹配用户的一个(否则添加违规)
- 如果用户被授予ROLE_ADMIN,那么$company是强制性的,但它可以是任何公司
这是我的StripeAccount实体:
<?php
namespace AppEntity;
use ApiPlatformCoreAnnotationApiProperty;
use ApiPlatformCoreAnnotationApiResource;
use AppRepositoryStripeAccountRepository;
use AppValidatorIsValidCompany;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;
use SymfonyComponentHttpFoundationFileFile;
use SymfonyComponentSerializerAnnotationGroups;
use SymfonyComponentSerializerAnnotationMaxDepth;
use SymfonyComponentValidatorConstraints as Assert;
use VichUploaderBundleMappingAnnotation as Vich;
/**
* @VichUploadable
* @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"
* }
* )
* @ORMEntity(repositoryClass=StripeAccountRepository::class)
*/
class StripeAccount
{
public const ACCOUNT_TYPE = 'custom';
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
* @Groups({"read:StripeAccount", "write:StripeAccount"})
*/
private $id;
/**
* @ORMManyToOne(targetEntity=Company::class, inversedBy="stripeAccounts")
* @ORMJoinColumn(nullable=false)
* @Groups({"read:StripeAccount", "admin:write"})
* @AssertNotBlank(groups={"admin:write"})
* @IsValidCompany
*/
private $company;
/**
* @ORMOneToMany(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 AppValidator;
use AppEntity{Company, User};
use SymfonyComponentSecurityCoreSecurity;
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintValidator;
class IsValidCompanyValidator extends ConstraintValidator
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function validate($value, Constraint $constraint)
{
/* @var $constraint AppValidatorIsValidCompany */
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 AppSerializer;
use ApiPlatformCoreSerializerSerializerContextBuilderInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityCoreAuthorizationAuthorizationCheckerInterface;
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。
我的问题是:我的@AssertNotBlank(groups={"admin:write"})被完全忽略。我尝试了一些调整@Groups注释,甚至denormalizationContext,但不,它没有在任何时候应用。我遗漏了什么?
顺便说一句,我使用邮差来测试我的API
谢谢你的帮助
[EDIT]基于@TekPike的回答,这是我的工作代码:
StripeAccount.php
<?php
namespace AppEntity;
use ApiPlatformCoreAnnotationApiProperty;
use ApiPlatformCoreAnnotationApiResource;
use AppRepositoryStripeAccountRepository;
use AppValidationAdminValidationGroupsGenerator;
use AppValidatorIsValidCompany;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;
use SymfonyComponentSerializerAnnotationGroups;
use SymfonyComponentSerializerAnnotationMaxDepth;
use SymfonyComponentValidatorConstraints 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",
* }
* )
* @ORMEntity(repositoryClass=StripeAccountRepository::class)
*/
class StripeAccount
{
/**
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
* @Groups({"read:StripeAccount", "write:StripeAccount"})
*/
private $id;
/**
* @ORMManyToOne(targetEntity=Company::class, inversedBy="stripeAccounts")
* @ORMJoinColumn(nullable=false)
* @Groups({"read:StripeAccount", "admin:write"})
* @AssertNotBlank(groups={"admin:write"})
* @IsValidCompany
*/
private $company;
/**
* @ORMColumn(type="string", length=255)
* @Groups({"read:StripeAccount", "write:StripeAccount"})
* @AssertNotBlank
*/
private $name;
// ...
}
和AdminValidationGroupsGenerator.php:
<?php
namespace AppValidation;
use ApiPlatformCoreBridgeSymfonyValidatorValidationGroupsGeneratorInterface;
use SymfonyComponentSecurityCoreAuthorizationAuthorizationCheckerInterface;
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"}}
和类AppSerializerAdminGroupsContextBuilder
定义序列化组。
但是,"admin:write"约束@AssertNotBlank(groups={"admin:write"})
中定义的group为验证组。
在您的例子中,由于验证组根据用户而变化,因此您必须使用动态验证组。