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