lundi 31 octobre 2016

Correction de PersistentCollection::clear()

Jusqu'à la version 2.5.5, PersistentCollection::clear() ne reset pas les clefs de $this->collection, si la collection est vide.

Ce qui veut dire que si on a eu des éléments dans $this->collection à un moment, puis qu'ils ont été supprimés "un à un" sans passer par clear(), la prochaine clef ne sera pas 0 mais l'index courant du tableau + 1 (2 si on avait 2 éléments par exemple).

Ce comportement pourrait être justifié, mais comme les clefs sont reset uniquement si on a des éléments à supprimer, le comportement dans les 2 cas n'est pas le même :
  • si on n'a pas d'éléments à supprimer, les clefs ne sont pas reset
  • si on a des éléments à supprimer, les clefs sont reset
$bar = new Entity(); $bar2 = new Entity(); $foo = new PersistentCollection(); $foo->add($bar); $foo->add($bar2); // les clefs seront reset, le prochain appel à add() aura l'index 0 $foo->clear(); $foo->add($bar); // [0] var_dump(array_keys($foo->toArray())); $foo = new PersistentCollection(); $foo->add($bar); $foo->add($bar2); $foo->removeElement($bar); $foo->removeElement($bar2); // les clefs ne seront pas reset, le prochain appel à add aura l'index suivant $foo->clear(); // [2] var_dump(array_keys($foo->toArray()));

PersistentCollection.php#536
Pull request #6110

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 { /** @var Collection|Comment[] */ protected $comments; public function __construct() { $this->comments = new ArrayCollection(); } /** @param Collection|Comment[] $comments */ public function setComments(Collection $comments): self { $this->clearComments(); foreach ($comments as $comment) { $this->addComment($comment); } return $this; } public function addComment(Comment $comment): self { 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): self { if ($this->comments->contains($comment)) { $this->comments->removeElement($comment); $comment->setUser(null); } return $this; } public function clearComments(): self { 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(null), 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 sauvegarde un User (à ne pas confondre avec la suppression d'un commentaire en particulier, qui s'effectue via orphanRemoval)
class Comment { /** @var ?User */ protected $user; public function setUser(User $user = null): self { $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ésasocité" 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 # uniquement si User::$id est également unsigned, ce qui devrait être le cas la plupart du temps options: unsigned: true
  • 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

jeudi 9 juin 2016

Passer de Symfony 2.8 à Symfony 3.1

Etant sur un projet qui a débuté en Symfony 2.6, j'ai du effectuer les migrations vers la 2.7, puis la 2.8.
Concrètement, à part un problème en 2.8.5 sur le NumberType qui retournait un int ou un float, et qui s'est mit à retourner uniquement des float (même si on a saisi 1 par exemple, voir le commit), tout s'est bien passé.

Par contre, et même en faisant très attention à ne pas trop avoir de deprecated dans notre code, et sachant que Symfony 2.8 lui-même générait des centaines de deprecated, le passage à la 3.1 ne s'est pas fait sans mal ... Déjà à cause des bundles utilisés. Il a fallu attendre quelques mois pour que les dépendances fonctionnent (FOSUserBundle par exemple), et certaines ne seront jamais mis à jour (knplabs/doctrine-behaviors).
Voici une liste de ce qui m'a posé soucis, et comment je l'ai corrigé :
  • $validator->validate($value, $constraints = null, $groups = null) : l'ordre des paramètre a changé. Avant, on pouvait passer $groups en 2ème, ou en 3ème, au choix. Depuis la 3.0, $groups est forcément le 3ème paramètre.
  • Knp\DoctrineBehaviors\ORM\Blameable\UserCallable utilisait security.context, qui n'existe plus, au profit de security.token_storage. Comme KNP s'occupe très peu de la migration vers Symfony 3, j'ai du changer la configuration knp.doctrine_behaviors.blameable_subscriber.user_callable.class, pour la faire pointer sur une classe de mon projet, qui utilise le nouveau service.
  • ContainerInterface::isScopeActive() n'existe plus, il a donc fallu faire sans
  • Dans les fichiers YML, il restait quelques chaines sans quotes, qui contenaient des ":". Depuis Symfony 3, le composant YML lève une exception de parsing dans ce cas-là.
  • Quelques configurations de FormType qui n'existent plus, comme pattern, precision, property, etc. Voir la liste ici.
  • Comme l'option choices_as_values n'existe plus, il a fallu repasser sur tous les CollectionType : pour les valeurs du sous-formulaire, avant on passait en clef la valeur à stocker, et en valeur, le libellé à afficher. Maintenant, c'est l'inverse.
  • {% render() %} n'aurait pas du être appelé comme ça, mais plutôt {{ render() }}. C'est obligatoire maintenant.
  • La configuration twig.form.resources devient twig.form_themes
  • La configuration security.firewalls.foo.form_login.csrf_provider: form.csrf_provider devient security.firewalls.foo.form_login.csrf_token_generator: security.csrf.token_manager

Benchmarks entre Symfony 2.8.4 et Symfony 3.1.0


Il faut bien prendre en compte que ces benchmarks sont à tire informatif. Ils ont été fait sur mon PC, donc avec une interface graphique très gourmande, avec Apache Bench, PHP 5.6, sur la page de connexion de mon projet.
Pour chacune des concurrences d'accès, 1 000 appels sont effectués. Le temps affiché est le temps moyen d'une requête. L'environnement est spécifié à côté de la version de Symfony.

Concurrence 1 Concurrence 3 Concurrence 5 Concurrence 10
Symfony 2.8.4
(prod)
81.329 ms
(12.30 req/s)
97.185 ms
(30.87 req/s)
114.103 ms
(43.82 req/s)
218.755 ms
(45.71 req/s)
Symfony 3.1.0
(prod)
113.321 ms
(8.82 req/s)
138.314 ms
(21.69 req/s)
152.142 ms
(32.86 req/s)
299.432 ms
(33.40 req/s)
Symfony 2.8.4
(dev)
230.995 ms 287.655 ms 342.507 ms 672.107 ms
Symfony 3.1.0
(dev)
243.499 ms 301.299 ms 358.609 ms 692.711 ms

On constate donc que l'environnement de prod est environ 3x plus rapide que l'environnement de dev.
Symfony 3.1 est 28% plus lent que Symfony 2.8.4 !

mardi 24 mai 2016

Mettre à jour son serveur MySQL en 5.7

Pour pouvoir utiliser les champs de type json en MySQL, il vous faut la version 5.7. Cependant, cette version étant assez récente, elle n'est pas fournie dans les dépôts d'origine sur toutes les distributions de Linux.

Voilà les commandes à executer pour mettre à jour votre serveur MySQL en 5.7.12, sans perdre les données (testées depuis une Ubuntu 14.04.4 LTS, qui a MySQL 5.6 dans les dépôts) :
wget http://dev.mysql.com/get/mysql-apt-config_0.7.2-1_all.deb sudo dpkg --install mysql-apt-config_0.7.2-1_all.deb
Choisissez MySQL Server au premier choix, puis la version 5.7 ensuite.
De retour au menu précédent, allez sur Ok (dernier choix) et validez.
sudo apt-get update && sudo apt-get upgrade mysql-server sudo mysql_upgrade -uroot -p --force sudo service mysql-server restart

Erreur 3065 suite à la migration

Si suite à cette mise à jour, vous avez ce type de d'erreur :
General error: 3065 Expression #1 of ORDER BY clause is not in SELECT list, references column
C'est un mode de requête qui est activé par défaut depuis la 5.7.5 (migration testée en 5.7.12) : ONLY_FULL_GROUP_BY.
Pour le supprimer, copiez ce que la requête suivante retourne :
SELECT @@GLOBAL.sql_mode;
Ensuite, ouvrez le fichier /etc/mysql/my.cnf, et sous la clef mysqld, ajoutez sql_mode = '' (en valeur : ce que vous avez copié, moins ONLY_FULL_GROUP_BY).

jeudi 21 avril 2016

Corriger les requêtes jouées en boucle par doctrine:schema:update

La commande doctrine:schema:update de Symfony permet de mettre à jour votre base de données, par rapport à votre mapping.
Dans certains cas, cette commande veut exécuter des requêtes qui n'ont pas lieu d'être.
De même si vous utilisez DoctrineMigrations, qui peut vous générer des migrations avec toujours les mêmes requêtes.

Par exemple, si vous voulez créér un type de champ FooDecimal, qui doit définir la valeur par défaut de scale à 2 au lieu de 0 :
Foo\Entity\Bar: fields: baz: type: foodecimal

Vous allez créer un type de champ Doctrine via cette documentation, avec entre autres ce code pour définir scale à 2 par défaut "dans la requête SQL" (pas au niveau mapping YML, XML ou PHP) :
class FooDecimalType extends DecimalType { public function getName() { return 'foodecimal'; } public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { $fieldDeclaration['scale'] = (empty($fieldDeclaration['scale'])) ? 2 : $fieldDeclaration['scale']; return $platform->getDecimalTypeDeclarationSQL($fieldDeclaration); } /** * Ajoute un commentaire SQL sur le champ (exemple MySQL : COMMENT '(DC2Type:foodecimal)'), avec le type retourné par getName() * Permet de lier le type de champ SQL decimal, au type de champ FooDecimalType * Sans ça, Doctrine se base sur la requête ALTER de création du champ pour "retrouver" le type de champ Doctrine * Le premier trouvé sera DecimalType, fourni avec Doctrine, et pas notre FooDecimalType * Comme le type de champ a changé pour Doctrine, une requête ALTER sera jouée à l'infini, * même si au final elle n'effectue pas de modification en base */ public function requiresSQLCommentHint(AbstractPlatform $platform) { return true; }
Au niveau de votre base de données, la valeur par défaut de scale sera bien 2.

Mais il est impossible de modifier les valeurs par défaut du côté du mapping, cf SchemaTool::gatherColumn(), et YamlDriver::columnToArray() pour le mapping YML par exemple.
Quand vous allez exécuter la commande doctrine:schema:update, SchemaTool::getSchemaFromMetadata() retournera 0 comme valeur pour scale pour votre champ baz du côté du mapping puisqu'il n'y a aucune information dans le fichier YML sur scale, alors que AbstractSchemaManager::createSchema() retournera 2, puisque votre champ en base de données est configuré pour avoir scale à 2.
A ce moment-là, pour Doctrine, le champ en base de données n'est pas le même que ce que le mapping lui indique. Donc, il génère une requête ALTER TABLE, qui modifiera le scale en base de données, pour lui mettre 2 (ce que FooDecimal définit dans la génération du SQL), bien que ce soir déjà le cas. La différence entre le mapping et la base de données est faite dans Doctrine\DBAL\Schema\Comparator::diffColumn().

Pour corriger ce problème de valeurs par défaut non modifiable du côté du mapping, on peut utiliser l'événement postGenerateSchemaTable, qui n'est pas documenté.
services: foo_decimal_default_scale: class: Foo\BarBundle\EventListener\FooDecimalDefaultScaleListener tags: - { name: doctrine.event_listener, event: postGenerateSchemaTable }
<?php namespace Foo\BarBundle\EventListener; use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; use Foo\BarBundle\Doctrine\Type\FooDecimal; class FooDecimalDefaultScaleListener { public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $event) { foreach ($event->getSchema()->getTables() as $table) { foreach ($table->getColumns() as $column) { if ($column->getType() instanceof FooDecimal && $column->getScale() === 0) { $column->setScale(2); } } } } }

mardi 12 avril 2016

Nouveautés dans Symfony 3.0 (novembre 2015)

Symfony 3.0 n'a aucune nouvelle fonctionnalité, comparé à la version 2.8.

Tout l'intérêt de cette version est de supprimer toutes les E_USER_DEPRECATED levées par Symfony au fil des versions.
Je n'ai pas retrouvé la source, mais de mémoire, sur le site de Symfony, ils disaient avoir supprimé 15% de lignes de code (quelques milliers quand même), pour arriver à ce résultat.
Comment passer de la 2.x à la 3.0

Pour vous aider à trouver les E_USER_DEPRECATED dans votre code, les corriger, et pouvoir passer à Symfony 3.0 :
symfony/phpunit-bridge
deprecation-detector
umpirsky/Symfony-Upgrade-Fixer

mardi 5 avril 2016

Créer un identifiant d'entité Doctrine

Doctrine 2.5 (et sûrement les versions antérieures) permet de mapper un champ d'une entité, en le définissant comme étant son identifiant.

La documentation parait complète, mais il y a quelques erreurs, et des informations de mapping qui ne sont pas reportées sur cette page.
Voici toutes les options possibles pour le mapping d'un identifiant, au format YML :
Foo\Entity\Bar: id: id: type: integer generator: strategy: NONE options: unsigned: false column: id associationKey: my_field length: 50 columnDefinition: INT AUTO_INCREMENT UNSIGNED sequenceGenerator: sequenceName: message_seq allocationSize: 100 initialValue: 1 customIdGenerator: Foo\CustomIfGenerator tableGenerator: Foo\TableGenerator
  • type : type du champ. Je n'ai pas testé tous les types de champs, certains peuvent ne pas fonctionner comme datetime. Liste des types de champs.
  • generator [défaut : NONE] : même si la documentation dit que la valeur par défaut est AUTO, c'est bien NONE la vraie valeur par défaut (cf ClassMetadataInfo, valeur par défaut de $generatorType). Donc par défaut, aucune gestion automatique de l'identifiant n'est effectuée, c'est à vous de faire setId() "avant le persist()". La valeur AUTO est la bonne pour la majorité des cas.
  • options : tableau d'options, chaque type de champ peut avoir ses options. Par exemple pour les types numériques, on peut spécifier unsigned.
  • column [défaut : id] : nom de la colonne dans la base de données.
  • associationKey : Voir la documentation.
  • length [défaut : 255] : utilisé pour les types string et binary, pour indiquer la longueur maximale de la valeur stockée en base.
  • columnDefinition : pour surcharger le code SQL généré dans le CREATE TABLE et ALTER TABLE.
  • sequenceGenerator : configuration de la séquence, utilisée uniquement pour Oracle et Postgres.
  • customIdGenerator : si aucune stratégie de génération d'identifiant ne vous convient, vous pouvez créer une classe qui doit étendre de Doctrine\ORM\Id\AbstractIdGenerator, et indiquer son fully qualified class name ici.
  • tableGenerator : petite blague de Doctrine, même si c'est écrit dans la documentation : la configuration existe, mais elle n'est pas gérée, et lève une MappingException.
Toutes ces informations proviennent de YamlDriver, de la version 2.5 de Doctrine, utilisée dans Symfony 2.8.

Pour résumer, voici la bonne configuration d'un mapping d'identifiant, pour la plupart des cas :

Foo\Entity\Bar: id: id: type: integer # de -2 147 483 648 à 2 147 483 647 en MySQL generator: strategy: AUTO # IDENTITY pour MySQL, SQLite, MsSQL et SQL Anywhere, SEQUENCE pour Oracle et PostgreSQL options: unsigned: true # pas d'identifiants négatifs, change le maximum à 4 294 967 295