Originally, the entity Gut had a field reaction that contained a string. The options for reaction were hard-wired in a template. By adding an entity Reaction and changing the Gut form's reaction to an EntityType I'm now plagued with the error message
SQLSTATE[42S22]: Column not found: 1054 Unknown column 't0.reaction' in 'field list'
even though I've rewritten the Gut & Reaction entities. I've probably lost sight of the forest for the trees. What's wrong with the following?
MySQL table gut: reaction column replaced by reaction_id; reaction_id correctly created; foreign key created manually.
Error occurs with this controller method:
#[Route('/', name: 'app_gut_index', methods: ['GET'])]
public function index(GutRepository $gutRepository): Response
{
$guts = $gutRepository->findBy([], ['happened' => 'DESC']); // error thrown here
return $this->render('gut/index.html.twig', [
'guts' => $guts,
]);
}
Gut entity:
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[ORM\ManyToOne(targetEntity: Reaction::class)]
#[ORM\JoinColumn(name: 'reaction_id', referencedColumnName: 'id')]
protected $reaction;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
#[ORM\Column(name: "datetime")]
private ?\DateTime $happened = null;
public function getId(): ?int
{
return $this->id;
}
public function getReaction(): ?Reaction
{
return $this->reaction;
}
public function setReaction(?Reaction $reaction): self
{
$this->reaction = $reaction;
return $this;
}
...
}
Reaction entity:
use App\Entity\Gut;
use App\Repository\ReactionRepository;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity(repositoryClass: ReactionRepository::class)]
class Reaction
{
public function __construct()
{
$this->guts = new ArrayCollection();
}
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 45)]
private ?string $reaction = null;
public function getId(): ?int
{
return $this->id;
}
public function getReaction(): ?string
{
return $this->reaction;
}
public function setReaction(string $reaction): self
{
$this->reaction = $reaction;
return $this;
}
#[ORM\OneToMany(targetEntity: Gut::class, mappedBy: 'reaction')]
private $guts;
/**
* #return Collection|Product[]
*/
public function getGuts(): Collection
{
return $this->guts;
}
public function addGut($gut): self
{
$this->guts[] = $gut;
return $this;
}
public function __toString()
{
return $this->getReaction();
}
}
Your $reaction property should not have both ORM\Column and ORM\JoinColumn annotations at the same time.
Because of this Doctrine thinks it's a regular column so it's looking for a database field based on the variable name: $reaction -> gut.reaction.
Remove #[ORM\Column(length: 255)] then make sure that you have gut.reaction_id in your database and now it should work.
As a little side note I don't think you need name: 'reaction_id', referencedColumnName: 'id' in ORM\JoinColumn because that's how Doctrine will name them automatically anyway
Just couldn't let go. I eventually found a path to get the Gut and Reaction entities to play nicely together. What I did:
cloned the project
manually deleted reaction property from Gut entity; created & executed a migration
in MySQL, added back in a reaction column
used make:entity Gut to add a reaction property as ManyToOne on Reaction; made a migration
used MySQL to populate the reaction_id column from the database of the cloned project.
(Probably missed a step in here somewhere, but) gut->getReaction(),etc,
now behave as expected - in a ManyToOne relationship.
Related
I have two entities, PartenairePermission and StructurePermission, I'm trying to get properties from the other entity in a One To Many relationship.
Which was working great with the one to one relationship but I modified it for a One To Many relationship and now I can't access the properties anymore
Attempted to call an undefined method named "setIsMembersRead" of class "Doctrine\ORM\PersistentCollection".
The idea of the script below is when the property is modified in PartenairePermission, it modified the property in StructurePermission too.
Any idea on how to solve this issue?
PartenaireController: [EDITED]
#[Route('/{id}/activate-permission', name: 'app_partenaire_activate-permission', methods: ['GET', 'POST'])]
public function activatePermission(EntityManagerInterface $entityManager, Request $request, PartenaireRepository $partenaireRepository, MailerInterface $mailer): Response
{
$partenairePermission = $entityManager->getRepository(PartenairePermission::class)->findOneBy([ // get the id of the partenaire
'id' => $request->get('id'),
]);
$partenairePermission->setIsMembersRead(!$partenairePermission->isIsMembersRead()); // set the value of the permission to the opposite of what it is ( for toggle switch )
$structurePermission = $partenairePermission->getPermissionStructure();
foreach ($structurePermission as $structurePermission) {
$structurePermission->setIsMembersRead($partenairePermission->isIsMembersRead());
}
$entityManager->persist($partenairePermission);
$entityManager->flush();
PartenairePermission.php :
#[ORM\OneToMany(mappedBy: 'permission_partenaire', targetEntity: StructurePermission::class, orphanRemoval: true)]
private Collection $permission_structure;
public function __construct()
{
$this->permission_structure = new ArrayCollection();
} /**
* #return Collection<int, StructurePermission>
*/
public function getPermissionStructure(): Collection
{
return $this->permission_structure;
}
public function addPermissionStructure(StructurePermission $permissionStructure): self
{
if (!$this->permission_structure->contains($permissionStructure)) {
$this->permission_structure->add($permissionStructure);
$permissionStructure->setPermissionPartenaire($this);
}
return $this;
}
public function removePermissionStructure(StructurePermission $permissionStructure): self
{
if ($this->permission_structure->removeElement($permissionStructure)) {
// set the owning side to null (unless already changed)
if ($permissionStructure->getPermissionPartenaire() === $this) {
$permissionStructure->setPermissionPartenaire(null);
}
}
return $this;
}
StructurePermission.php :
#[ORM\ManyToOne(fetch: "EAGER", inversedBy: 'permission_structure')]
#[ORM\JoinColumn(nullable: false)]
private ?PartenairePermission $permission_partenaire = null;
public function getPermissionPartenaire(): ?PartenairePermission
{
return $this->permission_partenaire;
}
public function setPermissionPartenaire(?PartenairePermission $permission_partenaire): self
{
$this->permission_partenaire = $permission_partenaire;
return $this;
}
Now you have to work different since you changed the association type:
$structurePermission = $partenairePermission->getPermissionStructure();
this will return a Collection (instead of a single Object as with your former One-to-One relationship).
and then something like:
foreach($structurePermission as $permission) {
// here you call your set/get/is for an Object within the Collection
}
I'm using PHP symfony with API-platform with JWT token (through LexikJWTAuthenticationBundle), latest version as of today.
I've read quite a lot of things and I know how to do the basic stuff:
Create an API exposing my entities,
Protect certain endpoints with JWT
Protecting certain endpoints with user_roles
What I'm trying to do now is to have the API only sends back data that belongs to a user instead of simply sending back everything contained in the database and represented by an entity. I've based my work on this but this does not take into account the JWT token and I don't know how to use the token in the UserFilter class : https://api-platform.com/docs/core/filters/#using-doctrine-orm-filters
Here is my Book entity :
<?php
// api/src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
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 Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use App\Entity\User;
use App\Attribute\UserAware;
/** A book. */
#[ORM\Entity]
#[ApiResource(operations: [
new Get(),
new GetCollection(),
new Post(),
new Put(),
new Patch(),
new Delete()
])]
#[UserAware(userFieldName: "id")]
class Book
{
/** The id of this book. */
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
/** The ISBN of this book (or null if doesn't have one). */
#[ORM\Column(nullable: true)]
#[Assert\Isbn]
public ?string $isbn = null;
/** The title of this book. */
#[ORM\Column]
#[Assert\NotBlank]
public string $title = '';
/** The description of this book. */
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
public string $description = '';
/** The author of this book. */
#[ORM\Column]
#[Assert\NotBlank]
public string $author = '';
/** The publication date of this book. */
#[ORM\Column(type: 'datetime')]
#[Assert\NotNull]
public ?\DateTime $publicationDate = null;
/** #var Review[] Available reviews for this book. */
#[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
public iterable $reviews;
#[ORM\Column(length: 255, nullable: true)]
private ?string $publisher = null;
/** The book this user is about. */
#[ORM\ManyToOne(inversedBy: 'books')]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
#[Assert\NotNull]
public ?User $user = null;
public function __construct()
{
$this->reviews = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getPublisher(): ?string
{
return $this->publisher;
}
public function setPublisher(?string $publisher): self
{
$this->publisher = $publisher;
return $this;
}
}
Here is my UserFilter class :
<?php
// api/src/Filter/UserFilter.php
namespace App\Filter;
use App\Attribute\UserAware;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Entity\User;
final class UserFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an attribute)
$userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;
$fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
if ($fieldName === '' || is_null($fieldName)) {
return '';
}
try {
$userId = $this->getParameter('id');
// Don't worry, getParameter automatically escapes parameters
} catch (\InvalidArgumentException $e) {
// No user id has been defined
return '';
}
if (empty($fieldName) || empty($userId)) {
return '';
}
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
}
}
Here is my UserAware class :
<?php
// api/Annotation/UserAware.php
namespace App\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class UserAware
{
public $userFieldName;
}
I added this to my config/packages/api_platform.yaml file:
doctrine:
orm:
filters:
user_filter:
class: App\Filter\UserFilter
enabled: true
It obviously does not work, since I'm not making the bridge between the JWT token and the filter, but I have no idea how to do it. What am I missing?
The current results I have is that the GET /api/books sends back all the books stored in the database instead of sending only the ones belonging to the JWT authenticated user.
EDIT:
And for those who want the answer for ManyToMany related entities here it is : Api-platform, filtering collection result based on JWT identified user on a ManyToMany relational entity
Instead of Doctrine Filter, you could use Doctrine Extension as described here.
In your case it would need:
Create the doctrine extension:
<?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 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 || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
return;
}
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
$queryBuilder->setParameter('current_user', $user->getId());
}
}
The main logic is in the addWhere() method:
applies only if you are dealing with Book entity (but you could extend the idea to a list of entities here)
check if the user is granted admin (if so here it skips the extension, allowing admin to fetch all books)
skip if the user isn't authenticated (you should prevent this access with firewall or security arribute in your endpoints)
Then it adds a where condition to the SQL query to filter by userId (or any other condition you'll need)
Don't forget to eanble your filter:
# api/config/services.yaml
services:
# ...
'App\Doctrine\CurrentUserExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
- { name: api_platform.doctrine.orm.query_extension.item }
I'm stuck for a while now in trying to get the following scenario to work.
I have a registration system for a sporting team.
Each team can register for any available contest. The problem is that each team is only allowed once to register on a contest. I simply want to filter the teams that are already registered for the current selected contest. I'm using the Symfony framework together with Doctrine ORM and the symfony form system.
I tried to create a custom function inside the teamRepository to only get the teams that are not registered for a given contest. But i can't seem to find a way to gt this to work.
I'm using 3 entities for this: User, Team, Registration, Contest
The registration entity holds all te submitted registrations with a relation to the Contest and the User (team). I hope that someone can help me here. For reference, this is my registration entity:
class Registrations
{
use TimestampableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'registrations')]
private $Team;
#[ORM\ManyToMany(targetEntity: Dancers::class, inversedBy: 'registrations', fetch: 'EAGER')]
private $Dancers;
#[ORM\Column(type: 'text', nullable: true)]
private $Comments;
#[ORM\ManyToOne(targetEntity: Contest::class, fetch: 'EAGER', inversedBy: 'registrations')]
#[ORM\JoinColumn(nullable: false)]
private $Contest;
#[ORM\Column(type: 'string', length: 255)]
#[Gedmo\Blameable(on: 'create')]
private $RegisteredBy;
#[ORM\OneToMany(mappedBy: 'Registration', targetEntity: Orders::class)]
private $orders;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $MusicFile;
public function __construct()
{
$this->Dancers = new ArrayCollection();
$this->orders = new ArrayCollection();
}
public function __toString()
{
return $this->id;
}
public function getId(): ?int
{
return $this->id;
}
public function getTeam(): ?Team
{
return $this->Team;
}
public function setTeam(?Team $Team): self
{
$this->Team = $Team;
return $this;
}
/**
* #return Collection|Dancers[]
*/
public function getDancers(): Collection
{
return $this->Dancers;
}
public function addDancer(Dancers $dancer): self
{
if (!$this->Dancers->contains($dancer)) {
$this->Dancers[] = $dancer;
}
return $this;
}
public function removeDancer(Dancers $dancer): self
{
$this->Dancers->removeElement($dancer);
return $this;
}
public function getComments(): ?string
{
return $this->Comments;
}
public function setComments(?string $Comments): self
{
$this->Comments = $Comments;
return $this;
}
public function getRegisteredBy(): ?string
{
return $this->RegisteredBy;
}
public function setRegisteredBy(?string $RegisteredBy): self
{
$this->RegisteredBy = $RegisteredBy;
return $this;
}
public function getContest(): ?Contest
{
return $this->Contest;
}
public function setContest(?Contest $Contest): self
{
$this->Contest = $Contest;
return $this;
}
/**
* #return Collection<int, Orders>
*/
public function getOrders(): Collection
{
return $this->orders;
}
public function addOrder(Orders $order): self
{
if (!$this->orders->contains($order)) {
$this->orders[] = $order;
$order->setRegistration($this);
}
return $this;
}
public function removeOrder(Orders $order): self
{
if ($this->orders->removeElement($order)) {
// set the owning side to null (unless already changed)
if ($order->getRegistration() === $this) {
$order->setRegistration(null);
}
}
return $this;
}
public function getMusicFile(): ?string
{
return $this->MusicFile;
}
public function setMusicFile(string $MusicFile): self
{
$this->MusicFile = $MusicFile;
return $this;
}
}
This is the repo function as mentioned:
public function getTeamsNotRegistered($organisation, $contest)
{
return $this->createQueryBuilder('t')
->leftJoin('t.Registrations', 'r')
->andWhere('t.Organisation = :org')
->andWhere('r.Contest = :contest')
->setParameter('org', $organisation)
->setParameter('contest', $contest)
->getQuery()
->getResult();
}
I'm getting this error which I think I get when trying to access any Entity Repository.
An exception has been thrown during the rendering of a template
("Cache key "App:Category__CLASSMETADATA__" contains reserved
characters "{}()/#:".").
I loaded the commit when the code last worked and reloaded but I'm still getting this error.
I have tried to find an answer in previous posts but those don't work for me because for example I don't have #Assert anywhere in the code.
I tried going through the composer.json file and update all libraries to latest version, then removing vendor folder and composer update because that worked for someone. But it hasn't worked for me.
I'm using Symfony 5.4 and PHP 8.0.
Category.php
<?php
namespace App\Entity;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 255)]
private $name;
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class, orphanRemoval: true)]
private $products;
public function __construct()
{
$this->products = 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, Product>
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setCategory($this);
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->removeElement($product)) {
// set the owning side to null (unless already changed)
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}
I'll be happy to share any information you think might be useful.
Thank you very much in advance.
Edit: I have just downloaded the commit in a different folder and rerun everything but still getting same error message.
Try to replace "App:Category" with "App\Entity\Category" when you call in controllers, forms and repositories.
We expirienced the same issue and the replace worked for us.
Each Product is "owned" by a given Tenant (i.e. user) and requires a color which could be either a standard Color available to all tenants or a proprietary TenantOwnedColor which was created by a given tenant and only available to that tenant.
#[ORM\Entity]
class Product implements BelongsToTenantInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 180)]
private string $name;
#[ORM\ManyToOne(targetEntity: Color::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Color $color;
#[ORM\ManyToOne(targetEntity: Tenant::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Tenant $tenant;
}
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(value: ['open' => Color::class, 'proprietary' => TenantOwnedColor::class])]
class Color
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 180)]
private string $name;
#[ORM\Column(type: 'string', length: 255)]
private string $colorCode;
}
#[ORM\Entity]
class TenantOwnedColor extends Color implements BelongsToTenantInterface
{
#[ORM\ManyToOne(targetEntity: Tenant::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Tenant $tenant;
}
In order to filter all entities that implement BelongsToTenantInterface and limit them to the Tenant that the logged on user belongs to, a listener adds a doctrine filter.
namespace App\EventListener;
use Doctrine\ORM\EntityManager;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;
final class AuthenticatedTenantEntityListener
{
public function __construct(private EntityManager $entityManager)
{
}
public function onJWTAuthenticated(JWTAuthenticatedEvent $jwtAuthenticatedEvent): void
{
$user = $jwtAuthenticatedEvent->getToken()->getUser();
if (!$user instanceof BelongsToTenantInterface) {
return;
}
$this->entityManager
->getFilters()
->enable('tenant_filter')
->setParameter('tenantId', $user->getTenant()->getId());
}
}
namespace App\Doctrine;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;
final class TenantFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $classMetadata, $targetTableAlias): string
{
if ($classMetadata->getReflectionClass()->implementsInterface(BelongsToTenantInterface::class)) {
return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->getParameter('tenantId'));
}
return '';
}
}
My approach works for Product but not for TenantOwnedColor. When troubleshooting, I discovered that TenantFilter::addFilterConstraint() is being passed the parent class (i.e. Color) metadata which doesn't implement BelongsToTenantInterface and thus I now know why it isn't filtering.
I also found the following in Doctrine's documentation so evidently it is by design:
In the case of joined or single table inheritance, you always get
passed the ClassMetadata of the inheritance root. This is necessary to
avoid edge cases that would break the SQL when applying the filters.
Are there other ways to implement this in order to overcome this shortcoming?
It seems that this topic has been brought up by the community some times now. There does not seem to be an official workaround, due to innestability provoked by those famous edge cases, although some people have made their changes/hacks/workarounds to the problem so it is not impossible.
Links that might help, with some workarounds mentioned in them, I hope you find them useful enough, sorry that I cannot be of more help:
https://github.com/doctrine/orm/issues/7504#issuecomment-568569307
https://github.com/doctrine/orm/issues/6329
https://github.com/doctrine/orm/issues/6329#issuecomment-538854316
https://www.doctrine-project.org/projects/doctrine-orm/en/2.11/reference/php-mapping.html#classmetadata-api