Why doctrine migrations ignore my index declaration? - symfony

I would like to create a table with an indexed columned to speed up searches.
Here is a sample:
#[ORM\Entity(repositoryClass: SettingRepository::class)]
#[ORM\Table(name: '`tr_setting`', indexes: [
new ORM\Index(columns: ['code'], name: 'idx_setting_code')
])]
class Setting
{
#[ORM\Column(type: 'string', length: 15)]
private ?string $code;
#[ORM\Column(type: 'text')]
private string $content;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
// Getter and setter...
}
When I use DoctrineBundle migration, the file is generated, but index is ignored...
// ....
final class Version20220719140604 extends AbstractMigration
{
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE "tr_setting_id_seq" INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE "tr_setting" (id INT NOT NULL, code VARCHAR(15) NOT NULL, content TEXT NOT NULL, PRIMARY KEY(id))');
}
For other projects, I already did it with annotations instead of attributes.
I carefully read this answer, but it doesn't help me.
I'm on PHP8.1.8, doctrine/orm 2.12.3 , doctrine-migrations: 3.2, postgresql: 13

My index shall be declared with this syntax:
#[
ORM\Entity(repositoryClass: SettingRepository::class),
ORM\Table(name: '`tr_setting`'),
ORM\Index(columns: ['code'], name: 'idx_setting_code'),
]
or this one:
#[ORM\Entity(repositoryClass: SettingRepository::class)]
#[ORM\Table(name: '`tr_setting`')]
#[ORM\Index(columns: ['code'], name: 'idx_setting_code')]

Related

Symfony - I get circular reference error in index of controller

Hi I'm having troubles getting a list of all "blog tag" entities in the index function of my controller.
I tried using normalizer groups but somehow I still get a circular reference error. I expect my controller to output a list of blog tags, by id and name.
This is my controller:
#[Route('/', name: 'api_blogtag_index', methods: ['GET'])]
#[IsGranted('IS_AUTHENTICATED')]
public function index(BlogtagRepository $blogtagRepository): Response
{
return $this->json([
'tags' => $blogtagRepository->findAll(),
Response::HTTP_OK, [], [
AbstractNormalizer::GROUPS => ['show_blogtag']
]
]);
}
And this is the blog tag entity class:
#[ORM\Entity(repositoryClass: BlogtagRepository::class)]
class Blogtag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['show_blogtag'])]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['show_blogtag'])]
private ?string $name = null;
#[ORM\ManyToMany(targetEntity: Blog::class, inversedBy: 'blogtags')]
private Collection $blogs;
Circular reference is an error that appears when an object refers to itself, directly.
This problem has already been solved, here you will find an answer of your problem.
You can learn more on official documentation

How to eliminate this Column not found error?

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.

DiscriminatorMap Symfony 5.4 change foreign key

I have this class
#[ORM\Entity]
#[ORM\InheritanceType('JOINED')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'integer')]
#[ORM\DiscriminatorMap([
1 => Successor1::class,
])]
abstract class Type
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private int $id;
public function getId(): int
{
return $this->id;
}
}
#[ORM\Entity]
class Successor1 extends Type
{
}
By default doctrine made foreign key by ID how to change it to like this
shema
Or type_id on type table

Relation with composite unique constraint (symfony + doctrine)

I'm trying to create relation where foreign key reference NOT to primary key but to composite unique constraint.
Why? Denormalize database schema for decrease join's count.
#[ORM\Entity(repositoryClass: CurrencyRepository::class)]
#[ORM\UniqueConstraint(fields: ['slug', 'type'])]
#[UniqueEntity(
fields: ['type', 'slug'],
message: 'This slug is already in use on that type.',
errorPath: 'slug',
)]
class Currency
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
#[ORM\Column(type: 'smallint', length: 1)]
private ?int $type;
#[ORM\Column(type: 'string', length: 25)]
private ?string $slug;
// ...
}
#[ORM\Entity(repositoryClass: ExchangeRateHistoryTypeRepository::class)]
class ExchangeRateHistoryType
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\ManyToOne(targetEntity: Currency::class)]
#[ORM\JoinColumn(name: 'currency_slug', referencedColumnName: 'slug', nullable: false)]
#[ORM\JoinColumn(name: 'currency_type', referencedColumnName: 'type', nullable: false)]
private ?Currency $currency;
php bin/console make:migration
php bin/console doctrine:migrations:migrate
All good. But when i try to add data to ExchangeRateHistoryType - error.
Client code:
$exchangeRateHistoryType = new ExchangeRateHistoryType();
$exchangeRateHistoryType->setCurrency($currency);
// ...
$this->entityManager->persist($exchangeRateHistoryType);
$this->entityManager->flush();
In BasicEntityPersister.php line 674: Warning: Undefined array key "slug"
What i'm doing wrong?
Doctrine's documentation:
It is not possible to use join columns pointing to non-primary keys. Doctrine will think these are the primary keys and create lazy-loading proxies with the data, which can lead to unexpected results. Doctrine can for performance reasons not validate the correctness of this settings at runtime but only through the Validate Schema command.
Source: https://www.doctrine-project.org/projects/doctrine-orm/en/2.10/reference/limitations-and-known-issues.html#join-columns-with-non-primary-keys

How to filter inherited Doctrine objects?

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

Resources