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
Related
I have a OneToMany relation that doesn't return items if the method stays as the return with Collection type
Account::class
.
.
#[ORM\OneToMany(mappedBy: 'account', targetEntity: Folder::class, orphanRemoval: true)]
public Collection $folders;
.
.
/**
* #return Collection<int, Folder>
*/
public function getFolders(): Collection
{
return $this->folders;
}
Folder::class
#[ORM\ManyToOne(inversedBy: 'folders')]
#[ORM\JoinColumn(name: 'account_id', referencedColumnName: 'id', nullable: false)]
public ?Account $account = null;
`
If I change the return type from getFolders to array and call $this->folders->toArray() the data is returned.
Is this related to EAGER and LAZY? I can't understand what doctrine is doing here.
I thought that since it is a code generalized by symfony itself, the return with the Collection type should have the same result. Where am I getting lost?
There are two entities: Product and ProductDetails:
class ProductDetail {
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'productDetail')]
#[ORM\JoinColumn(nullable: false)]
private $productItem;
#[ORM\Column(type: 'date', nullable: true)]
#[Assert\GreaterThan('today')]
#[Assert\Expression(
expression: 'this.getProductItem()->getStatus() in ['not done'] or value',
message: 'The planification date cannot be null for this status!',
)]
private $finishedDate;
class Product {
#[ORM\Column(type: 'string', length: 255)]
private $status;
Basically I want to create a constraint for the finishedDate to not allow to be empty when the status is 'not done'.
But it doesn't do absolutely nothing and I'm curious if I'm missing anything. Even if I put value there and the date is empty the message is not triggered.
According to this link : https://symfony.com/doc/current/components/expression_language/syntax.html.
Could you try :
this.getProductItem().getStatus()
You can use a custom constraint in your case, it's more flexible :
https://symfony.com/doc/current/validation/custom_constraint.html
I am using Gedmo for it's Timestampable options, to automatically add created_at and updated_at dates but getting error:
Integrity constraint violation: 1048 Column 'created_at' cannot be null
Why they are not filled? That's how I am implementing it:
#[ORM\Column]
#[Gedmo\Timestampable(on: 'create')]
private ?\DateTime $createdAt;
#[ORM\Column]
#[Gedmo\Timestampable]
private ?\DateTime $updatedAt;
Doing the things as per the documentation. Am I missing something ?
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')]
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