Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions doc/decisions/ADR-003-doctrine-entity-mapped-superclass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
Id: ADR-003
Date: 2026-06-29
Statut: Proposé
---

# Les entités Doctrine héritent d'une classe parente

## Contexte

Avec l'ajout de la baseline PHPStan et le passage au niveau 10, il y a pas mal d'endroits dans le code où l'id nullable
des entités pose problème.

Par exemple, quand on récupère une liste d'entités depuis un repository, on sait que l'id est présent, mais pas PHPStan
car la propriété reste nullable dans la classe de l'entité.

Cela force des vérifications qui n'apportent pas grand chose et rendent le code plus difficile à lire et naviguer.

Par exemple :

```php
#[ORM\Entity]
#[ORM\Table(name: 'exemple')]
class Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(length: 255, nullable: true)]
public string $foo = null;
}

class ExampleRepository
{
/**
* @return array<Entity>
*/
public function all(): array { /* return ... */ }
}

$entities = $exempleRepository->all();

$map = [];
foreach ($entities as $entity) {
// Cette ligne va déclencher une erreur PHPStan car l'id pourrait être nullable,
// alors qu'on sait ici que ce n'est pas le cas.
$map[$entity->id] = $entity->foo;

// Il faudrait faire ça à chaque fois :
if ($entity->id === null) {
continue;
}

$map[$entity->id] = $entity->foo;
}
```

## Décision

Les entités Doctrine héritent d'une classe abstraite contenant l'id et une méthode pour vérifier sa présence.

Cela permet à PHPStan de mieux analyser le code, tout en conservant une certaine sécurité. Si une entité n'est pas
persistée et qu'on tente de lire son id, cela déclenche une erreur.

### Détails d'implémentation

```php
use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'exemple')]
class Exemple extends Entity
{
#[ORM\Column(length: 255, nullable: false)]
public string $nonNullbale;

#[ORM\Column(length: 255, nullable: true)]
public ?string $nullable = null;
}
```

Et à l'utilisation :

```php
if ($exemple->isPersisted()) {
// $exemple->id est initialisé et non-null
}
```

## Alternatives considérées

1. **Un trait** : C'est plus difficile et lent à analyser pour PHPStan qu'une classe parente.
2. **Vérifier l'id à chaque fois** : Le code devient moins lisible pour peu d'intérêt.

## Conséquences

### Positives

Quand on récupère une ou plusieurs entités depuis la base de données, plus besoin de vérifier la présence de l'id dans
l'instance.

Si on tente d'accéder à l'id d'une entité à un endroit non vérifié, une erreur survient fort et au bon endroit (au lieu
de trimballer un `null` plus loin dans le code).

### Négatives

Toutes les entités doivent hériter d'une classe parente.

Cela ne fonctionne qu'avec des entités qui ont un id entier auto-incrément.

## Références

Analyse des traits par PHPStan : https://phpstan.org/blog/how-phpstan-analyses-traits

Exemple PHPStan : https://phpstan.org/r/3f3354d7-d6f5-4493-bfbd-3f7a37cf8d32
12 changes: 0 additions & 12 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,6 @@
'count' => 1,
'path' => __DIR__ . '/sources/Afup/Corporate/Page.php',
];
$ignoreErrors[] = [
'message' => '#^Parameter \\#1 \\$parentId of method AppBundle\\\\Site\\\\Entity\\\\Repository\\\\FeuilleRepository\\:\\:getFeuillesEnfant\\(\\) expects int, int\\|null given\\.$#',
'identifier' => 'argument.type',
'count' => 3,
'path' => __DIR__ . '/sources/Afup/Corporate/Page.php',
];
$ignoreErrors[] = [
'message' => '#^Possibly invalid array key type int\\|null\\.$#',
'identifier' => 'offsetAccess.invalidOffset',
'count' => 1,
'path' => __DIR__ . '/sources/Afup/Corporate/Page.php',
];
$ignoreErrors[] = [
'message' => '#^Cannot access offset \'elements\' on mixed\\.$#',
'identifier' => 'offsetAccess.nonOffsetAccessible',
Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_compte')]
class Account
class Account extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(name: 'nom_compte', length: 45, nullable: false)]
public string $name;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_categorie')]
class Category
class Category extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(name: 'categorie', length: 255, nullable: false)]
public string $name;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_evenement')]
class Event
class Event extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(name: 'evenement', length: 50, nullable: false)]
public string $name;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_operation')]
class Operation
class Operation extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(name: 'operation', length: 255, nullable: false)]
public string $name;
}
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_reglement')]
class Payment
class Payment extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(name: 'reglement', length: 50, nullable: false)]
public string $name;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Produit.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@
namespace AppBundle\Accounting\Entity;

use AppBundle\Accounting\TvaTaux;
use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_produit')]
class Produit
class Produit extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(length: 255, nullable: false)]
public string $reference;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Accounting/Entity/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Accounting\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'compta_regle')]
class Rule
class Rule extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(length: 255, nullable: false)]
public string $label;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/AssembleeGenerale/Entity/Presence.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@

use AppBundle\AssembleeGenerale\Enum\PresenceEtat;
use AppBundle\Association\Entity\Utilisateur;
use AppBundle\Doctrine\Entity;
use AppBundle\Doctrine\Type\UnixTimestampType;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'afup_presences_assemblee_generale')]
class Presence
class Presence extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(type: UnixTimestampType::NAME, nullable: false)]
public \DateTime $date;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/AssembleeGenerale/Entity/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@

use AppBundle\AssembleeGenerale\Enum\QuestionEtat;
use AppBundle\AssembleeGenerale\Enum\VoteValeur;
use AppBundle\Doctrine\Entity;
use AppBundle\Doctrine\Type\UnixTimestampType;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'afup_assemblee_generale_question')]
class Question
class Question extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(type: UnixTimestampType::NAME, nullable: true)]
public ?\DateTime $date = null;

Expand Down
8 changes: 2 additions & 6 deletions sources/AppBundle/Association/Entity/Utilisateur.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace AppBundle\Association\Entity;

use AppBundle\Doctrine\Entity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'afup_personnes_physiques')]
class Utilisateur
class Utilisateur extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\Column(length: 255, nullable: true)]
public ?string $email = null;
}
21 changes: 21 additions & 0 deletions sources/AppBundle/Doctrine/Entity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace AppBundle\Doctrine;

use Doctrine\ORM\Mapping as ORM;

#[ORM\MappedSuperclass]
abstract class Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int $id;

public function isPersisted(): bool
{
return isset($this->id);
}
}
8 changes: 2 additions & 6 deletions sources/AppBundle/Site/Entity/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@

namespace AppBundle\Site\Entity;

use AppBundle\Doctrine\Entity;
use AppBundle\Doctrine\Type\UnixTimestampType;
use AppBundle\Site\Enum\ArticleTheme;
use AppBundle\Site\Enum\ArticleEtat;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'afup_site_article')]
class Article
class Article extends Entity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;

#[ORM\ManyToOne(targetEntity: Rubrique::class)]
#[ORM\JoinColumn(name: 'id_site_rubrique', referencedColumnName: 'id', nullable: true)]
public ?Rubrique $rubrique = null;
Expand Down
Loading
Loading