doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.86k stars 2.5k forks source link

UniqueConstraint worked on annotation on Parent class, not anymore after using Attribute #11421

Open rudy286 opened 2 months ago

rudy286 commented 2 months ago

BC Break Report

Q A
BC Break yes
Version 2.19.4
php 8.1
DB PostgrSQL
Symfony 6.4

Summary

During the upgrade from php 7.4 to php 8.1 _(same DB server, with same databaseurl, from sf 5.4 to 6.4), I have a break while converting Annotation to Attribute: does not work for ORM\UniqueConstraint.

Note: My application extend the user (utilisateur) class from my custom bundle (it is an important detail as we will see in the end):

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use My\MyBundle\Entity\Utilisateur as BaseUtilisateur;

#[ORM\Table(name: '`utilisateur`')]
#[ORM\Entity(repositoryClass: \App\Repository\UtilisateurRepository::class)]
class Utilisateur extends BaseUtilisateur
{
}

Previous behavior

//...

/**
 * @ORM\MappedSuperclass
 * @ORM\HasLifecycleCallbacks
 * @ORM\Table(
 *  name="utilisateur",
 *  uniqueConstraints={
 *    @ORM\UniqueConstraint(
 *      columns={"identifiant"},
 *      options={"where": "est_actif = true"}
 *    )
 *  },
 * )
 */
class Utilisateur implements UserInterface, EquatableInterface
{
// ...

This code would produce this migration (php bin/console make:migration):

// ...
    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        // ...
        $this->addSql('CREATE UNIQUE INDEX UNIQ_1D1C63B3C90409EC ON utilisateur (identifiant) WHERE est_actif = true');
   // ...
}

Current behavior

With attributs, now the code produces this migration (php bin/console make:migration):

// ...
    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE UNIQUE INDEX UNIQ_1D1C63B3C90409EC ON "utilisateur" (identifiant)');
   // ...
}

How to reproduce

In my class user I tried 2 way to write the UniqueConstraint:

  1. combined with Table:
    
    //...

[ORM\MappedSuperclass]

[ORM\HasLifecycleCallbacks]

[ORM\Table(name: 'utilisateur', uniqueConstraints: [

new ORM\UniqueConstraint(
    columns: ['identifiant'],
    options: ["where" => "est_actif = true"],
    )

])] class Utilisateur implements UserInterface, EquatableInterface { // ...

 2. separated from Table (default when using [rector](https://github.com/rectorphp/rector-symfony) to automatically migrate anotations to attributes:
```php
//...

#[ORM\MappedSuperclass]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(
    name: "utilisateur",
)]
#[ORM\UniqueEntity(
    columns: ["identifiant"],
    options: ["where" => "est_actif = true"],
)]
class Utilisateur implements UserInterface, EquatableInterface
{
// ...
  1. I even tryed to remove the prefix ORM to be more "compliant" with the documentation:
//...
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\UniqueConstraint; // I've checked, this file exists in folder vendor

#[ORM\MappedSuperclass]
#[ORM\HasLifecycleCallbacks]
#[Table(
    name: "utilisateur",
)]
#[UniqueEntity(
    columns: ["identifiant"],
    options: ["where" => "est_actif = true"],
)]
class Utilisateur implements UserInterface, EquatableInterface
{
// ...

According to the documentation, the parameter fieldis required, so I added it, still produce the not expected migration

#[Table(
    name: "utilisateur"
)]
#[UniqueConstraint(
    fields: ["identifiant"],
    columns: ["identifiant"],
    options: ["where" => "est_actif = true"]
)]

I explicitly told doctrine to use attibutes:


## config/packages/doctrine.yaml
### ...
    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: false
        mappings:
            App:
                is_bundle: false
                type: attribute
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

Note: If I update the child class 'user' (utilisateur) it will work as the following code shows, BUT, I need my bundle to works for all apps using MyBundle:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use My\MyBundle\Entity\Utilisateur as BaseUtilisateur;

#[ORM\Table(name: '`utilisateur`')]
#[ORM\Entity(repositoryClass: \App\Repository\UtilisateurRepository::class)]
#[ORM\UniqueConstraint(
    name: 'uniq_identifiant_actif',    // here I added a name, or it will conflict with parent constraint, i just want to check what happend if i created a constraint here for the science
    columns: ['identifiant'],
    options: ['where' => 'est_actif = true']
)]
class Utilisateur extends BaseUtilisateur
{
}

And the migration created:

// ...
    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE UNIQUE INDEX UNIQ_1D1C63B3C90409EC ON "utilisateur" (identifiant)'); // this comes from the  is the attribut of the parent user class: FAIL 
        $this->addSql('CREATE UNIQUE INDEX uniq_identifiant_actif ON "utilisateur" (identifiant) WHERE est_actif = true'); // this comes from the attribute of the child user class: SUCCESS

This suggest myBundle can't hold the description of the user entity of all my apps using the bundle.
It looks like all the apps using the same bundle must explicitly tells ORM how to migrate correctly the constraint (why would i create a bundle to extend the parent class then?)

Do you confirm there is a break/regression here, or it is something else?
Any hint welcome !

thanks