Make a field #Assert\NotBlank only for admin - php

This is my first time posting, my apologies if I don't follow some rules.
I'm using API Platform in my Symfony 5.3 project. I'm trying to make a field in one of my entities writable with some rules. The entity is called StripeAccount and must be linked to a $company object (see mapping below). Here are the rules
If the user is NOT granted ROLE_ADMIN, then the $company is not mandatory as it will be automatically filled
If the user is NOT granted ROLE_ADMIN and provide the $company, it MUST match the user's one (or else a violation is added)
If the user IS granted ROLE_ADMIN, then the $company IS mandatory but it can be any company
This is my StripeAccount entity :
<?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
}
I followed this tutorial : https://symfonycasts.com/screencast/api-platform-security/context-builder#play (chapters 25, and 33 to 36), so I have this validator :
<?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();
}
}
}
and this 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;
}
}
Everything works fine, the group 'admin:write' is added if the user making the request is an admin, and the $company is set if the user is not an admin.
My problem is :
My #Assert\NotBlank(groups={"admin:write"}) is completly ignored. I tried a few adjustments with the #Groups annotation and even the denormalizationContext, but no, it's not applied at any moment. What am I missing here?
Btw, I'm using Postman to test my API
Thanks a lot for your help
[EDIT] Based on #TekPike's answer, here is my working code:
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;
// ...
}
And my 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];
}
}

You confuse serialization groups with validation groups.
Currently you define serialization groups with the annotation denormalizationContext={"groups"={"write:StripeAccount"}} and the class App\SerializerAdminGroupsContextBuilder.
However, the "admin:write" group defined in the constraint #Assert\NotBlank(groups={"admin:write"}) is a validation group.
In your case, since the validation group changes depending on the user, you have to use dynamic validation groups.

Related

Persist releted resource fields using api-platform

I'm using API-Platform for a projetc. And in this project, I have two resources (Donateur and Don) releted by a ManyToOne relation. One Don is releted to One Donateur and One Donateur can be releted to more Dons. I have a data persister which persists resources in database.
Donateur.php
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\DonateurRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ApiResource(
* normalizationContext={
* "groups"={
* "read:Donateur:collection"
* }
* },
* denormalizationContext={
* "groups"={
* "write:Donateur"
* }
* },
* collectionOperations={
* "get", "post"
* },
* itemOperations={
* "get", "patch", "delete"
* }
* )
* #ORM\Entity(repositoryClass=DonateurRepository::class)
*/
class Donateur
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups("read:Donateur:collection")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups({"read:Donateur:collection", "write:Donateur"})
*/
private $nom;
/**
* #ORM\Column(type="float")
* #Groups({"read:Donateur:collection", "write:Don"})
*/
private $solde;
/**
* #ORM\OneToMany(targetEntity=Don::class, mappedBy="donateur")
*/
private $dons;
Don.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\DonRepository;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ApiResource(
* normalizationContext={
* "groups"={
* "read:Don:item",
* "read:Don:collection"
* }
* },
* denormalizationContext={
* "groups"={
* "write:Don"
* }
* },
* collectionOperations={
* "get", "post"
* },
* itemOperations={
* "get"={
* "normalization_context"={
* "groups"={
* "read:Don:item",
* "read:Don:collection"
* }
* }
* },
* "patch", "delete"
* }
* )
* #ORM\Entity(repositoryClass=DonRepository::class)
*/
class Don
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=Donateur::class, inversedBy="dons")
* #ORM\JoinColumn(nullable=false)
* #Groups({"read:Don:collection", "write:Don"})
*/
private $donateur;
/**
* #ORM\Column(type="date", nullable=true)
* #Groups({"read:Don:collection", "write:Don"})
*/
private $date;
/**
* #ORM\Column(type="float")
* #Groups({"read:Don:collection", "write:Don"})
*/
private $montant;
The property $solde in Donateur must increase when we save a new Don. And the value which must be added is value of $montant of Don resource.
When I try this code
<?php
namespace App\EventListener;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\Don;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
class UpdateSoldeDonateurSubscriber implements EventSubscriberInterface
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ["updateSoldeDonateur", EventPriorities::POST_WRITE]
];
}
public function updateSoldeDonateur(ViewEvent $event)
{
$data = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$user = $this->security->getUser();
//On va mettre à jour le solde du donateur. On récupère d'abord l'ancien solde puis on y ajoute le nouveau
if($data instanceof Don){
$ancienSolde = $data->getDonateur()->getSolde();
$nouveauSolde = $ancienSolde + $data->getMontant();
if($method === 'POST'){
$data->getDonateur()->setSolde($nouveauSolde);
}
elseif($method === 'PATCH'){
}
// dd($data->getDonateur(), $ancienSolde, $nouveauSolde);
}
}
}
solde didn't change. I think that it is caused by the data persister but I don't know how can I fix it.
These are my datapersisters
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\Donateur;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
class DonateurDataPersister implements ContextAwareDataPersisterInterface
{
private $manager;
private $security;
public function __construct(EntityManagerInterface $manager, Security $security)
{
$this->manager = $manager;
$this->security = $security;
}
public function supports($data, array $context = []): bool
{
return $data instanceof Donateur;
}
public function persist($data, array $context = [])
{
$this->manager->persist($data);
$this->manager->flush();
return $data;
}
public function remove($data, array $context = [])
{
$data->setIsDeleted(true);
$data->setDeletedBy($this->security->getUser());
$data->setDeletedAt(new \DateTime());
$this->manager->flush();
}
}
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\Don;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
class DonDataPersister implements ContextAwareDataPersisterInterface
{
private $manager;
private $security;
public function __construct(EntityManagerInterface $manager, Security $security)
{
$this->manager = $manager;
$this->security = $security;
}
public function supports($data, array $context = []): bool
{
return $data instanceof Don;
}
public function persist($data, array $context = [])
{
$this->manager->persist($data);
$this->manager->flush();
return $data;
}
public function remove($data, array $context = [])
{
$data->setIsDeleted(true);
$data->setDeletedBy($this->security->getUser());
$data->setDeletedAt(new \DateTime());
$this->manager->flush();
}
}
Some ideas to help me please
Your subscriber have been called after writing to the database and you should to flush after making changes in it or use another event that called before writing (PRE_WRITE, in example), as I mentioned in the comment.

Doctrine entities reference each other's primary key

I have an Account entity which contains a collection of Group entities. In addition, each Account must have a default Group. How can one persist the entities? When attempting to do so, $manager->flush(); complains with a Not null violation for the entity that was persisted first.
Account:
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class Account
{
/**
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class=UuidV4Generator::class)
*/
private $id;
/**
* #ORM\OneToOne(targetEntity=Group::class, cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false, unique=true)
*/
private $defaultGroup;
/**
* #ORM\OneToMany(targetEntity=Group::class, mappedBy="account")
*/
private $groups;
public function __construct()
{
$this->groups = new ArrayCollection();
$this->defaultGroup = new Group();
}
// Typical getters and setters
}
Group:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class Group
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=Account::class, inversedBy="groups")
* #ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
private $account;
// Typical getters and setters
}
Your approach is not possible because Group::$account is already mapped to the one-to-many Account::$groups property. Doctrine does not know what to do when you persist a Group via Account::$defaultGroup, as this is not the property Group::$account is inversing.
You will have to enforce this Account default group invariant yourself programmatically. There's multiple approaches to this, but I suggest using the existing Account::$groups mapping in combination with the Symfony Collection validation constraint. Validating that an Account should have at least 1 Group assigned to it, which will be the default one.
Here's an example:
Because the entity name Group was giving me syntax problems in the SQL dialect I was using I renamed it to Team.
The Account implementation:
<?php declare(strict_types = 1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Account
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #ORM\OneToMany(
* targetEntity="App\Entity\Team",
* mappedBy="account",
* cascade={"persist", "remove"},
* )
* #Assert\Count(
* min=1
* )
*/
private Collection $teams;
public function __construct()
{
$this->teams = new ArrayCollection();
$this->addTeam(new Team());
}
public function getId(): ?int
{
return $this->id;
}
public function getDefaultTeam(): ?Team
{
$team = $this->teams->first();
return $team instanceof Team ? $team : null;
}
public function getTeams(): Collection
{
return $this->teams;
}
public function setTeams(array $teams): void
{
$this->teams->clear();
foreach ($teams as $team) {
if ($team instanceof Team) {
$this->addTeam($team);
}
}
}
public function addTeam(Team $team): void
{
if (false === $this->teams->contains($team)) {
$this->teams->add($team);
$team->setAccount($this);
}
}
public function removeTeam(Team $team): void
{
if (true === $this->teams->contains($team)) {
$this->teams->removeElement($team);
}
}
}
The Team implementation:
<?php declare(strict_types = 1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Team
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #ORM\ManyToOne(
* targetEntity="App\Entity\Account",
* inversedBy="teams",
* )
* #ORM\JoinColumn(
* name="account_id",
* referencedColumnName="id",
* nullable=false
* )
*/
private ?Account $account = null;
public function getId(): ?int
{
return $this->id;
}
public function getAccount(): ?Account
{
return $this->account;
}
public function setAccount(Account $account): void
{
$this->account = $account;
}
}

Filter an array collection relation

I try to filter the collection returned by a parent Entity base on several conditions that refer to another Entity (User).
Basicaly i want API PLATFORM to return the messages from the current connected User only.
Here is my Entity named File (very simplified for global understanding)
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Annotation\Groups;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use App\Controller\FileCreatePdfController;
use Ramsey\Uuid\Uuid;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* #ApiResource(
*
* normalizationContext={"groups"={"file"},"enable_max_depth"=true},
* denormalizationContext={"groups"={"file-write"},"enable_max_depth"=true},
* attributes={"force_eager"=false},
* )
* #ApiFilter(SearchFilter::class, properties={"status": "exact"})
* #ApiFilter(DateFilter::class, properties={"updatedAt"})
* #ORM\Entity
* #ORM\Table(name="cases")
*/
class File
{
public function __construct()
{
$this->id = Uuid::uuid4();
$this->messages = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
/**
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
*/
private $id;
/**
* #var Collection|FileMessage[]
*
* #Groups({"file"})
* #ORM\OneToMany(targetEntity="App\Entity\FileMessage", mappedBy="file")
*/
private $messages;
/**
* #return Collection|FileMessage[]
*/
public function getMessages(): Collection
{
return $this->messages;
}
/**
* #param FileMessage $message
* #return File
*/
public function addMessage(FileMessage $message): self
{
if (false === $this->messages->contains($message)) {
$this->messages->add($message);
}
return $this;
}
}
My file contains some messages from FileMessage (very simplified for global understanding
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Ramsey\Uuid\Uuid;
/**
* #ApiResource(
* normalizationContext={"groups"={"file-message"}, "enable_max_depth"=true},
* denormalizationContext={"groups"={"file-message-write"}, "enable_max_depth"=true},
* attributes={"force_eager"=false}
* )
* #ORM\Entity
* #ORM\Table(name="`file_messages`")
*/
class FileMessage {
/**
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
*/
private $id;
/**
* #var File
*
* #ORM\ManyToOne(targetEntity="File", cascade={"persist"}, inversedBy="messages")
* #Assert\Valid
* #Groups({"file-message", "file-message-write","file"})
*/
private $file;
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", cascade={"persist"}, inversedBy="messages")
* #Assert\Valid
* #Groups({"file-message", "file-message-write","file"})
*/
private $user;
public function __construct()
{
$this->id = Uuid::uuid4();
}
public function getId()
{
return $this->id;
}
public function getFile(): ?File
{
return $this->file;
}
public function setFile(?File $file): self
{
$this->file = $file;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}
Each message is posted by a specific user (another entity User ) i don't think it is neccesary to post the content of this entity.
When i fetch a specific file , url/files/[file_id]
All the messages for all users are displayed , i want to hide all message that not refer to the connected User
Solution I have tried so far :
Extensions , I only filter the /files that i retreive not the messages collection
Custom filters , does not seems to be appropriated
Collection filtering , seems perfect only when you filter on current class properties , in my case i need to filter the properties based on another class content (User).
Do I have any solution ? I was thinking maybe to use an event listener
Extensions are the way to go. They give you access to the Doctrine ORM Query Builder, so you can filter both the current resource and its relations by tweaking the WHERE clause of the DQL query.
It's always better to filter the data as early as possible (for performance, among other reasons), so here doing it at the DBMS level is the best option.

How to use default API Platform filters with custom action?

I have custom action (saw in docs as recommended method) that makes some logic and returns doctrine collection of entities.
With regular api-platform action filters working perfectly. But how can i get any from default filters to work with this collection in my custom action?
When i request GET /cars?createdAt[after]=2018-08-01 or GET /drivers?createdAt[after]=2018-08-01 it works as expected.
But when i'm trying to do GET /drivers/42/cars_custom_logic?createdAt[after]=2018-08-01 it doesn't filter anything. It's expected as i didn't call filter in my custom action, but my question is – how to add this filter?
App\Entity\Car
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity
* #ApiResource
* #ApiFilter(DateFilter::class, properties={"createdAt"})
*/
class Car
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups({"car", "driver"})
*/
private $id;
/**
* #ORM\Column(type="datetime")
* #Groups({"car", "driver"})
*/
private $createdAt;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Driver", inversedBy="cars")
* #Groups({"car", "driver"})
*/
private $driver;
public function __construct()
{
$this->createdAt = new \DateTime('now');
}
public function getId(): int
{
return $this->id;
}
public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}
public function getDriver(): Driver
{
return $this->driver;
}
}
App\Entity\Driver
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity
* #ApiResource(itemOperations={
* "get",
* "special"={
* "method"="GET",
* "path"="/drivers/{id}/cars_custom_logic",
* "controller"=GetDriverCarsAction::class
* }
* })
* #ApiFilter(DateFilter::class, properties={"createdAt"})
*/
class Driver
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups({"car", "driver"})
*/
private $id;
/**
* #ORM\Column(type="datetime")
* #Groups({"car", "driver"})
*/
private $createdAt;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Car", mappedBy="driver")
* #Groups({"car", "driver"})
*/
private $cars;
public function __construct()
{
$this->createdAt = new \DateTime('now');
}
public function getId(): int
{
return $this->id;
}
public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}
/**
* #return Collection|Car[]
*/
public function getCars(): Collection
{
return $this->cars;
}
}
App\Controller\GetDriverCarsAction
<?php
namespace App\Controller;
use App\Entity\Car;
use App\Entity\Driver;
use Doctrine\Common\Collections\Collection;
use Symfony\Bridge\Doctrine\RegistryInterface;
final class GetDriverCarsAction
{
private $doctrine;
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
public function __invoke(Driver $driver): Collection
{
$cars = $driver->getCars();
// ..... Some domain logic .....
// ..... Here – what should i do to make filter work here? .....
return $cars;
}
}
What if you try adding via yaml like this:
# api/config/api_platform/resources.yaml
App\Entity\Book:
attributes:
filters: [ offer.date_filter ]
itemOperations:
get: ~
special:
method: 'GET'
path: '/books/{id}/special'
controller: 'App\Controller\BookSpecial'
OR
# api/config/api_platform/resources.yaml
App\Entity\Book:
itemOperations:
get: ~
special:
method: 'GET'
path: '/books/{id}/special'
controller: 'App\Controller\BookSpecial'
filters: ['offer.date_filter']
For more deep look at this documentation: https://api-platform.com/docs/core/filters#doctrine-orm-filters
Hope it helps

The target-entity "some entity" cannot be found

i am using ZF2 with doctrine i am getting this error.
The target-entity Entity\User cannot be found in 'Subject\Entity\Subject#user'.
Here is the snippet to my code.
<?php
namespace Subject\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Factory as InputFactory;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
* #ORM\Entity
* #ORM\Table(name="subject")
* #property string $subjectname
* #property int $user_id
* #property int $id
*/
class Subject implements InputFilterAwareInterface {
protected $inputFilter;
/**
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $subjectname;
/**
* #ORM\ManyToOne(targetEntity="Entity\User", inversedBy="subjects")
* #var User|null
*/
private $user;
/** #return User|null */
public function getUser() {
return $this->user;
}
/** #param User $user */
public function setUser(User $user) {
if($user === null || $user instanceof User) {
$this->user = $user;
} else {
throw new InvalidArgumentException('$user must be instance of Entity\User or null!');
}
}}
and then my "User" entity
namespace Subject\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Factory as InputFactory;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/*
* #ORM\Entity
* #ORM\Table(name="users")
* #property string $username
* #property string $password
* #property int $id
*/
class User implements InputFilterAwareInterface {
protected $_username;
protected $_password;
/**
* #ORM\OneToMany(targetEntity="Entity\Subject", mappedBy="user")
* #var Collection
*/
private $subjects;
/** #ORM\Id() #ORM\Column(type="integer") #ORM\GeneratedValue(strategy="AUTO") #var int */
protected $_id;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
//Getters and setters
/** #return Collection */
public function getSubjects() {
return $this->comments;
}
/** #param Comment $comment */
public function addSubject(Subject $comment) {
$this->comments->add($comment);
$comment->setUser($this);
}
}
Your entity declaration is incorrect:
* #ORM\ManyToOne(targetEntity="Entity\User", inversedBy="subjects")
This should be either this:
* #ORM\ManyToOne(targetEntity="Subject\Entity\User", inversedBy="subjects")
Or, since the two classes share the same namespace, you can also use this:
* #ORM\ManyToOne(targetEntity="User", inversedBy="subjects")
The targetEntity has to be the fully qualified class name (FQCN), except if referring to a class in the same namespace, in which case the short name may be used (as per the last example above).
In our case, the file name was not the same as the class name: it was just a typo.
I had the same problem. The problem in my case was case-sensitivity.
Bad:
class Package {
/**
* #ORM\OneToMany(targetEntity="PackagUSA", mappedBy="package")
*/
private Collection $packageusas;
}
class Packageusa {
/**
* #ORM\ManyToOne(targetEntity="Package", inversedBy="packageusas")
* #ORM\JoinColumn(name="package_id", referencedColumnName="id")
*/
private Package $package;
}
Good:
class Package {
/**
* #ORM\OneToMany(targetEntity="PackageUSA", mappedBy="package")
*/
private Collection $packageusas;
}
class PackageUSA {
/**
* #ORM\ManyToOne(targetEntity="Package", inversedBy="packageusas")
* #ORM\JoinColumn(name="package_id", referencedColumnName="id")
*/
private Package $package;
}

Categories