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
2 changes: 1 addition & 1 deletion app/config/reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@
* enabled?: bool|Param, // Default: false
* },
* markdown?: bool|array{
* enabled?: bool|Param, // Default: false
* enabled?: bool|Param, // Default: true
* },
* intl?: bool|array{
* enabled?: bool|Param, // Default: true
Expand Down
123 changes: 123 additions & 0 deletions doc/decisions/ADR-003-doctrine-id-non-nullable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
Id: ADR-003
Date: 2026-06-29
Statut: Proposé
---

# Les entités Doctrine ont un id non-nullable

## 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 (!isset($entity->id)) {
continue;
}

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

## Décision

Dans une entité Doctrine, la propriété `id` est déclarée non-nullable.

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
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public int $id;

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

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

Et à l'utilisation :

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

## Alternatives considérées

1. **Une classe parente** : Cela force toutes les entités à hériter d'une classe, et, limite aux tables qui ont un id auto-incrément.
2. **Un trait** : C'est plus difficile et lent à analyser pour PHPStan qu'une classe parente.
3. **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

Il faut faire un peu plus attention en manipulant des entités, car tenter de lire un id qui ne serait pas présent
déclenche une erreur.

## 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
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Account
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'nom_compte', length: 45, nullable: false)]
public string $name;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Category
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'categorie', length: 255, nullable: false)]
public string $name;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Event
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'evenement', length: 50, nullable: false)]
public string $name;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Operation
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'operation', length: 255, nullable: false)]
public string $name;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Payment
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'reglement', length: 50, nullable: false)]
public string $name;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Produit.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Produit
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(length: 255, nullable: false)]
public string $reference;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Accounting/Entity/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Rule
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(length: 255, nullable: false)]
public string $label;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/AssembleeGenerale/Entity/Presence.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Presence
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(type: UnixTimestampType::NAME, nullable: false)]
public \DateTime $date;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/AssembleeGenerale/Entity/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Question
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(type: UnixTimestampType::NAME, nullable: true)]
public ?\DateTime $date = null;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Association/Entity/Utilisateur.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Utilisateur
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(length: 255, nullable: true)]
public ?string $email = null;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Site/Entity/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Article
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\ManyToOne(targetEntity: Rubrique::class)]
#[ORM\JoinColumn(name: 'id_site_rubrique', referencedColumnName: 'id', nullable: true)]
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Site/Entity/Feuille.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Feuille
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(nullable: true)]
public ?int $idParent = null;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Site/Entity/Rubrique.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Rubrique
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(nullable: true)]
public ?int $idParent = null;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/SuperApero/Entity/SuperApero.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SuperApero
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(type: 'date_immutable', nullable: false)]
public DateTimeImmutable $date;
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/SuperApero/Entity/SuperAperoMeetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class SuperAperoMeetup
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\ManyToOne(targetEntity: SuperApero::class, inversedBy: 'meetups')]
#[ORM\JoinColumn(nullable: false)]
Expand Down
32 changes: 21 additions & 11 deletions sources/AppBundle/SuperApero/Form/SuperAperoType.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,33 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
}
});

$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($antennes): void {
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
/** @var SuperApero $superApero */
$superApero = $event->getData();
$form = $event->getForm();

if (isset($superApero->date)) {
$year = $superApero->annee();
$existing = $this->superAperoRepository->findOneByYear($year);
if (!isset($superApero->date)) {
return;
}

if ($existing !== null && $existing->id !== $superApero->id) {
$form->get('date')->addError(
new FormError("Un Super Apéro existe déjà pour l'année {$year}."),
);
}
$year = $superApero->annee();
$existing = $this->superAperoRepository->findOneByYear($year);

if ($existing === null) {
return;
}

if (!isset($superApero->id) || $existing->id !== $superApero->id) {
$event->getForm()->get('date')->addError(
new FormError("Un Super Apéro existe déjà pour l'année {$year}."),
);
}
});

$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($antennes): void {
/** @var SuperApero $superApero */
$superApero = $event->getData();

$meetupsForm = $form->get('meetups');
$meetupsForm = $event->getForm()->get('meetups');

$submittedAntennes = [];
foreach ($antennes as $antenne) {
Expand Down
2 changes: 1 addition & 1 deletion sources/AppBundle/Veille/Entity/Envoi.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Envoi
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(name: 'sending_date', type: 'datetime')]
public \DateTimeInterface $dateEnvoi;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class NewsletterDesinscription
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\Column(length: 255, nullable: true)]
public ?string $email = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class NewsletterInscription
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
public ?int $id = null;
public int $id;

#[ORM\ManyToOne(targetEntity: Utilisateur::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true)]
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/site/article_form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{% block content %}
<h2>{{ formTitle }}</h2>

{% if article.slug != '-' %}
{% if article.id is defined %}
<div class="ui menu">
<div class="ui simple dropdown item" tabindex="0">
<i class="icon linkify"></i>
Expand Down
Loading