I'm using Symfony api-platform in their latest versions as of today.
I've got a User Entity and a Team entity which are related through a ManyToMany ORM relation. A User can have several Teams a Team can have several Users, the Team "owns" the relation.
Once my user is logged in through a JWT Token, I would like the endpoint GET /teams to only send back the Teams in which the identified User is part of.
Here is my User entity :
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ApiResource(operations: [
new Get(),
new GetCollection()
])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
/** @var book[] Available reviews for this book. */
#[ORM\OneToMany(targetEntity: Book::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
public iterable $books;
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'users')]
private Collection $teams;
public function __construct()
{
$this->teams = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/**
* Méthode getUsername qui permet de retourner le champ qui est utilisé pour l'authentification.
*
* @return string
*/
public function getUsername(): string {
return $this->getUserIdentifier();
}
/**
* @return Collection<int, Team>
*/
public function getTeams(): Collection
{
return $this->teams;
}
public function addTeam(Team $team): self
{
if (!$this->teams->contains($team)) {
$this->teams->add($team);
}
return $this;
}
public function removeTeam(Team $team): self
{
$this->teams->removeElement($team);
return $this;
}
}
Here is my Team Entity :
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\TeamRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\User;
#[ORM\Entity(repositoryClass: TeamRepository::class)]
#[ApiResource(security: "is_granted('ROLE_USER')")]
class Team
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
private ?string $name = null;
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'teams')]
private Collection $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* @return Collection<int, User>
*/
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users->add($user);
$user->addTeam($this);
}
return $this;
}
public function removeUser(User $user): self
{
if ($this->users->removeElement($user)) {
$user->removeTeam($this);
}
return $this;
}
}
Here is my CurrentUserExtension Class that filters result based on the current user :
<?php
// api/src/Doctrine/CurrentUserExtension.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\Book;
use App\Entity\Team;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if ((Book::class !== $resourceClass && Team::class !== $resourceClass) || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
if (Team::class == $resourceClass){
$queryBuilder->andWhere(sprintf('%s.users = :current_user', $rootAlias));
//trigger_error($queryBuilder);
}
$queryBuilder->setParameter('current_user', $user->getId());
}
}
Obviously it doesn't work because of the nature of the relation existing between the two tables. I also tried to use the query builder to use leftjoin and join the user_team table. But since the user_team table is not an Entity it failed.
Here is an SQL equivalent of what I would like to get as a result :
select * from team t
left join user_team ut on ut.team_id = t.id
where user_id = :current_user
CodePudding user response:
You should be able to just check if your user is IN
or MEMBER OF
one of your team $users.
Your query was close, try updating it with:
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if ((Book::class !== $resourceClass && Team::class !== $resourceClass) || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
if (Team::class == $resourceClass){
$queryBuilder->andWhere(sprintf(':current_user MEMBER OF %s.users', $rootAlias));
$queryBuilder->setParameter('current_user', $user);
}
}
You could either use MEMBER OF
or IN
but in a manyToMany it's usually easier to use MEMBER OF
.