mercredi 5 octobre 2016

Comment bien coder une manyToOne bidirectionnelle

Doctrine2 permet de lier ses entités via 2 types de liaisons : manyToOne et oneToOne.
La liaison manyToMany n'est qu'un raccourci d'une entité A vers une entité "invisible" via une manyToOne, puis une oneToMany vers votre entité B.
Toutes les liaisons peuvent être unidirectionnelles (exemple : une manyToOne n'a pas de oneToMany associée) ou bidirectionnelles (exemple : une manyToOne a une oneToMany associée).
Dans le cas des liaisons bidirectionnelles, il faut définir qui est le propriétaire (owning side), et le côté inverse (inverse side).
  • manyToOne / oneToMany : c'est la manyToOne qui est le owning side, et la oneToMany le inverse side
  • oneToOne : il faut choisir une des deux entités comme owning side, en ajoutant inversedBy côté owning side et mappedBy côté inverse side. Seule la table du owning side aura une clef étrangère vers le inverse side.
  • manyToMany : il faut choisir une des deux entités comme owning side, en ajoutant inversedBy côté owning side et mappedBy côté inverse side. Une table de liaison sera créé par Doctrine2
Plus d'informations sur les liaisons Doctrine2

Doctrine2 ne gère que le owning side


Doctrine2 ne gère que le owning side des relations : manyToOne, manyToMany owning side, et oneToOne owning side.
La raison est toute simple : dans votre base de données, le côté owning side est celui qui contient la clef étrangère.
Prenons pour exemple une entité User, qui est liée à une entité Comment : User > oneToMany > Comment.
Au niveau de votre base de données, c'est la table comment qui aura un user_id.
Le fait d'ajouter des Comment sur l'entité User sans appeler Comment::setUser() ne sert à rien. Doctrine2 ne sauvegardera pas votre Comment avec la liaison vers User, et conservera donc null dans Comment::$user.
Au final, une requête de ce type sera générée :
INSERT INTO comment (user_id, message) VALUES (null, 'mon commentaire')
Grace à cette exemple, on comprend qu'un inverse side (oneToMany, oneToOne inverse side, manyToMany inverse side) n'est finalement qu'un lien pratique pour le développeur, et pas un lien réellement géré par Doctrine2.

Coder la oneToMany "parfaite"


Beaucoup de bugs peuvent ne pas se voir rapidement quand on code une oneToMany : tentative d'insertion du owning side avec null dans la clef étrangère, suppression des objets côté PHP mais pas en base de données etc.
Reprenons notre exemple User > oneToMany > Comment :
class User { protected Collection $comments; public function __construct() { $this->comments = new ArrayCollection(); } /** @param iterable<mixed, Comment> $comments */ public function setComments(iterable $comments): static { $this->clearComments(); foreach ($comments as $comment) { $this->addComment($comment); } return $this; } public function addComment(Comment $comment): static { if ($this->comments->contains($comment) === false) { $this->comments->add($comment); $comment->setUser($this); } return $this; } /** @return Collection<Comment> */ public function getComments(): Collection { return $this->comments; } public function removeComment(Comment $comment): static { if ($this->comments->contains($comment)) { $this->comments->removeElement($comment); $comment->setUser(); } return $this; } public function clearComments(): static { foreach ($this->getComments() as $comment) { $this->removeComment($comment); } $this->comments->clear(); return $this; } }
  • Utiliser Collection de partout, sauf dans __construct() : compatibilité avec ArrayCollection et PersistentCollection, objet qu'on peut retrouver dans les formulaires via le type entity par exemple
  • Les PHPDoc avec Collection|Comment[] pour avoir l'auto-complétion à la fois des méthodes de Collection, et de l'objet Comment
  • setComments() ne doit surtout pas faire $this->comments = new ArrayCollection(), sinon, on ne supprime pas les liaisons du owning side Comment ! Il faut bien appeler clearComments() et addComment()
  • addComment() doit vérifier que le Comment qu'on veut ajouter n'existe pas déjà, pour éviter tout doublon
  • addComment() doit appeler Comment::setUser(), pour que le owning side Comment ait connaissance de la valeur à mettre dans la clef étrangère user_id
  • removeComment() doit appeler Comment::setUser(), pour que le côté owning side Comment sache qu'il n'est plus lié à un User
  • clearComments() ne doit pas faire $this->comments = new ArrayCollection(), sinon, on ne supprime pas les liaisons du owning side Comment ! Il faut appeler removeComment()
  • clearComments() doit remettre à 0 le pointeur du tableau interne de Collection, via clear(). Sinon, le prochain appel à add() n'ajoutera pas le commentaire en clef 0, mais en clef 2 (si clearComments() a supprimé les commentaires 0 et 1 par exemple). /!\ Avant Doctrine 2.5.6, l'appel à Collection::reset() pouvait ne pas remettre à 0 les clefs, si le tableau ne contenait pas d'élément.
User: oneToMany: comments: targetEntity: Comment mappedBy: user orphanRemoval: true cascade: [persist, remove]
  • mappedBy est obligatoire côté inverse side, pour indiquer quel est le champ lié du côté ownig side
  • orphanRemoval est un peu "illogiquement placé" : il permet de dire à Doctrine2 de supprimer tous les Comment qui ont null dans Comment::$user. Cette configuration aurait peut-être due être mise côté owning side Comment, mais c'est comme ça
  • cascade persist pour sauvegarder les Comment quand on sauvegarde un User
  • cascade remove pour supprimer tous les Comment quand on supprime un User (à ne pas confondre avec la suppression d'un commentaire en particulier, qui s'effectue via orphanRemoval)
class Comment { protected ?User $user; public function __construct() { $this->user = null; } public function setUser(User $user = null): static { $this->user = $user; if ($user instanceof User) { $user->addComment($this); } return $this; } public function getUser(): ?User { return $this->user; } }
  • Comment::$user peut être du type User, ou null, quand on a "désassocié" un Comment d'un User
  • setUser() peut donc accepter une instance de User, ou null
  • getUser() peut donc retourner une instance de User, ou null
Comment: manyToOne: user: targetEntity: User inversedBy: comments joinColumn: nullable: false
  • inversedBy est obligatoire du côté owning side, si on on veut avoir une bidirectionnelle. Sinon, il ne faut pas l'indiquer.
  • joinColumns.nullable doit être à false si on ne veut pas qu'un commentaire puisse avoir user_id à null
  • grâce à orphanRemoval du côté inverse side, tous les Comment qui ont null dans Comment::$user seront supprimés

lundi 26 septembre 2016

PHP a 2 types de tableaux

Derrière ce titre racoleur se cache une vérité : PHP gère bien 2 types de tableaux. Une version "standard", avec des clefs numériques, et une version "associative", avec des clefs type string.
Documentation PHP sur array

Comment PHP passe d'un tableau standard à un tableau associatif, sans nous le dire, et sans qu'on puisse le voir même avec un var_dump() ?
Si on ajoute des valeurs dans un tableau qui n'a que des clefs numériques, qui partent de 0, et qui n'ont pas de trou, alors c'est un tableau standard.
Du moment qu'un tableau a des trous dans ses clefs numériques, ou qu'on ajoute une clef type string, alors c'est un tableau associatif.

Un auto-cast en int des clefs est effectué en interne. Ce qui veut dire qu'une clef '0' sera automatiquement transformée en intval('0'), et notre tableau final n'aura pas une clef type string mais bien type int.
Cet auto-cast supprime également les parties décimales des float. Par exemple, une clef 0.5 sera transformée en 0, alors qu'une clef '0.5' restera bien telle quelle.

Quelques exemples pour illustrer cette explication :
// tableau standard $array = [ 'foo', 'bar' ]; // type de tableau non modifié, c'est encore un tableau standard $array[] = 'baz'; // typage transparent en tableau associatif $array[10] = 'tou'; // tableau standard, même si on met une chaine en clef // comme c'est '0', c'est casté en int en interne. la clef '0' devient 0. $array2 = [ '0' => 'foo' ]; // tableau associatif, on n'a pas de clef pour 2 $array3 = [ 0 => 'foo', 1 => 'bar', 3 => 'baz' ]; // tableau standard, l'auto-cast des clefs transforme 1.5 en 1 $array4 = [ 0 => 'foo', 1.5 => 'bar' ]; // tableau associatif, la clef '1.5' conserve son type string $array5 = [ 0 => 'foo', '1.5' => 'bar' ];

La réaction de json_encode() aux 2 types de tableaux


Pour se rendre compte de tout ça, un appel à json_encode($array) peut-être effectué avec les cas de test ci-dessus :
  • Quand le retour est un tableau, c'est un tableau standard
  • Quand le retour est un objet, c'est un tableau associatif
Cette différence est très importante.
Par exemple, pour les cas $array3 et $array5, si on fait un bête json_decode(json_encode($array3)), on n'obtient pas un array, mais un \stdClass !

lundi 19 septembre 2016

Issue #6042 : lazy loading inutile si on définit getId() dans un trait

Lorsqu'on veut accéder à un identifiant d'une entité qui n'est pas encore chargée par Doctrine, et qu'on passe donc par un proxy, aucun lazy loading n'est effectué parceque le Proxy contient déjà l'identifiant. N'importe quel accès à une autre propriété effectuera un lazy loading.

Si on définit la méthode getId() d'une entité dans un trait, lors de l'appel à getId(), un lazy loading sera effectué. Le Proxy généré ne comprend pas que la méthode getId() n'accède qu'à l'identifiant, et a le même comportement que n'importe quelle autre accesseur.

Issue #6042

lundi 5 septembre 2016

Issue #19860 : les constructeurs des Listeners ne sont pas appelés suivant la priorité des services

Dans le fichier var/cache/classes.php, généré par Symfony, une méthode protected function lazyLoad($eventName) est créée.

Cette méthode appelle tous les constructeur de tous les listeners d'un même événement, avant même que la méthode liée à l'événement (onKernelRequest par exemple) du listener ayant la priorité la plus haute soit appelée.
Ces constructeurs ne sont pas appelés selon la priorité des services, mais selon l'ordre d'enregistrement dans le Container.
De plus, comme tous les constructeurs sont appelés avant les méthodes liées à l'événement, on ne peut pas avoir un listener A qui créé / modifie une donnée dont aura besoin le listener B dans son constructeur.

Issue #19860

lundi 22 août 2016

mercredi 10 août 2016

Sélectionner certains champs d'une table avec Doctrine

Si vous voulez sélectionner seulement certains champs d'une table, parmis les champs mappés, vous pouvez utiliser le mot-clef PARTIAL :
<?php /** @Entity */ class Foo { /** @Column(type="integer") */ protected $id; /** @Column(type="string", length=140) */ protected $name; }
<?php use Doctrine\ORM\EntityRepository; class FooRepository extends EntityRepository { public function bar() { $query = $this ->createQueryBuilder('foo') ->select('PARTIAL foo.{id}') ->getQuery(); var_dump($query->getSQL()); return $query->getResult(); } }
Le SQL affiché par var_dump() sera de ce type :
SELECT c0_.id as id_0 FROM foo

Ne pas récupérer les liaisons (oneToMany, etc)


Si on ajoute une liaison quelconque à l'entité Foo, même avec PARTIAL dans la requête, cette liaison sera ajoutée dans la requête :
<?php /** @Entity */ class Foo { /** @Column(type="integer") */ protected $id; /** @Column(type="string", length=140) */ protected $name; /** @OneToMany(targetEntity="Comments", mappedBy="foo") */ protected $comments; }
SELECT c0_.id as id_0, c0_.comments as comments_1 FROM foo

Pour la supprimer réellement, il faut ajouter le hint Query::HINT_FORCE_PARTIAL_LOAD suivant à votre requête :
<?php use Doctrine\ORM\Query; use Doctrine\ORM\EntityRepository; class FooRepository extends EntityRepository { public function bar() { $query = $this ->createQueryBuilder('foo') ->select('PARTIAL foo.{id}') ->getQuery(); $query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true); return $query->getResult(); } }
SELECT c0_.id as id_0 FROM foo

Documentation à propos des hints
SqlWalker qui gère le hint HINT_FORCE_PARTIAL_LOAD (ligne 705)

vendredi 1 juillet 2016

Nouveautés dans Symfony 3.2 (novembre 2016)

Voici une liste exhaustive des nouveautés apportées par la version 3.2 de Symfony :
  • File controller helper : ajout d'une méthode file(), qui permet de créer une Response demandant le téléchargement du fichier
  • Compiler passes improvements : ajout d'une priorité pour l'ordre d'appel des CompilerPass, et d'un trait PriorityTaggedServiceTrait pour récupérer les servives taggés ordonnés par leur priorité
  • PHP constantes in YAML files : ajout de !php/const: pour les constantes PHP et liées à un objet
  • DateInterval form type : ajout d'un FormType DateInterval
  • Tagged Cache : ajout d'un système d'invalidation de cache (pour le Cache component ajouté en 3.1), via des tags
  • File controller helper : ajout d'une méthode file(), qui permet de créer une Response demandant le téléchargement du fichier
  • Compiler passes improvements : ajout d'une priorité pour l'ordre d'appel des CompilerPass, et d'un trait PriorityTaggedServiceTrait pour récupérer les servives taggés ordonnés par leur priorité
  • PHP constantes in YAML files : ajout de !php/const: pour les constantes PHP et liées à un objet
  • Routing Improvements : ajout de l'ancre dans l'url générée (#foo) et ajout du support des arrays pour les paramètres des routes au format XML
http://symfony.com/blog/new-in-symfony-3-2-console-improvements-part-1 http://symfony.com/blog/new-in-symfony-3-2-console-improvements-part-2 http://symfony.com/blog/new-in-symfony-3-2-better-readability-for-yaml-numeric-literals http://symfony.com/blog/new-in-symfony-3-2-lazy-loading-of-form-choices http://symfony.com/blog/new-in-symfony-3-2-user-value-resolver-for-controllers http://symfony.com/blog/new-in-symfony-3-2-httpfoundation-improvements http://symfony.com/blog/new-in-symfony-3-2-workflow-component http://symfony.com/blog/new-in-symfony-3-2-added-support-for-xpath-expressions http://symfony.com/blog/new-in-symfony-3-2-unicode-routing-support http://symfony.com/blog/new-in-symfony-3-2-improved-private-services http://symfony.com/blog/new-in-symfony-3-2-yaml-deprecations http://symfony.com/blog/new-in-symfony-3-2-filesystem-improvements http://symfony.com/blog/new-in-symfony-3-2-runtime-environment-variables http://symfony.com/blog/new-in-symfony-3-2-web-debug-toolbar-and-profiler-improvements http://symfony.com/blog/new-in-symfony-3-2-console-improvements-part-3 http://symfony.com/blog/new-in-symfony-3-2-csv-and-yaml-encoders-for-serializer http://symfony.com/blog/new-in-symfony-3-2-dx-improvements http://symfony.com/blog/new-in-symfony-3-2-cache-improvements http://symfony.com/blog/new-in-symfony-3-2-firewall-config-class-and-profiler

mardi 28 juin 2016

Benchmarks Symfony

Symfony n'est pas connu pour être rapide (et c'est pas forcément là son but non plus).
Voilà quelques informations sur les différentes versions, du répertoire vendor/symfony, récupérées via AlDanial/cloc :

2.3.42 2.4.10 2.5.12 2.6.13 2.7.14 2.8.7 3.0.7 3.1.1
Fichiers 4 034 3 451 4 578 4 798 4 835 4 966 4 520 4 684
Lignes 561 903 344 694 602 693 632 603 654 992 675 141 615 327 629 976

Informations sur le nombre de fichiers, classes et interfaces inclus pour arriver au code d'une action de Controller (chiffre de gauche), puis à la toute fin du fichier app.php (chiffre de droite) :

2.3.42 2.4.10 2.5.12 2.6.13 2.7.14 2.8.7 3.0.7 3.1.1
Fichiers 86-152 81-160 91-162 101-173 107-180 119-183 114-210 122-218
Classes 294-341 294-351 306-356 317-369 313-365 315-361 311-371 321-381
Interfaces 74-93 77-99 77-98 81-101 82-103 87-105 86-115 93-122

En conclusion :
  • Pas loin de 59 000 lignes de code supprimées entre la 2.8 et la 3.0 (la 3.0 est la copie de la 2.8, avec toutes le code déprécié en moins)
  • Il faut tout de même 218 fichiers, 381 classes et 122 interfaces pour afficher un Hello World en 3.1
  • La 3.1 est 28% plus lente que la 2.8, ce qui est difficilement compréhensible