Symfony1 et les formulaires imbriqués avec Doctrine

De e-glop
Révision datée du 4 juin 2013 à 10:32 par BeTa (discussion | contributions) (Une relation many-to-one, plusieurs objets liés)

Symfony, quand il est adossé à Doctrine pour l'accès aux données, permet d'accéder à une fonctionnalité très pratique lorsque l'on veut avoir un seul et même formulaire pour un objet et une ou plusieurs dépendances :

sfFormDoctrine::embedRelation()

La difficulté est que la documentation n'est pas satisfaisante à mes yeux. Voici donc un complément d'information :

Posons deux contextes différents

Un seul objet lié, one-to-one

Nous avons alors un schéma de ce type :

Picture:
  columns:
    name:
      type: string
      notnull: true
      notblank: true
    type:
      type: string(255)
      notnull: true
      notblank: true
    content:
      type: blob
      notnull: true
    width: integer
    height: integer
Group:
  columns:
    name:
      type: string
      unique: true
      notblank: true
      notnull: true
    picture_id:
      type: integer
      unique: true
  relations
    Picture:
      foreignAlias: Groups
      onDelete: SET NULL
      onUpdate: CASCADE

Des groupes étant illustrés ou non par une image, issue d'un modèle extérieur au groupes de manière à pouvoir être réutilisé par ailleurs... pas très complexe en soit. La plus grosse particularité est que la méthode sfFormDoctrine::embedRelation() est prévue pour partir d'un objet Picture alors que là nous partirons d'un objet Group...

Une relation one-to-many, plusieurs objets liés

YOB:
  columns:
    year:
      type: integer
      notnull: true
    month: integer
    day: integer
    name: string(255)
    contact_id: integer
  relations:
    Contact:
      foreignAlias: YOBs
      onDelete: CASCADE

Nous ne précisons pas le modèle Contact car cela a peu d'intérêt.

Une relation many-to-many, relations multiples

À venir ...


Essayons et constatons les difficultés

one-to-one, avec des images et des groupes

La particularité est que nous avons un formulaire de téléchargement de fichier à partir duquel la majorité des champs seront précisés. Essayons pour voir :

// lib/form/doctrine/PictureForm.class.php
class PictureForm extends BasePictureForm
{
 // a hack for blob content which was erased on form initialization
 public function __construct(Picture $object = NULL)
 {
   if (!( $object instanceof Picture ))
     return parent::__construct();
   
   $buf = $object->content;
   $r = parent::__construct($object);
   if ( $object->content !== $buf )
     $object->content = $buf;
   
   return $r;
 }
 public function configure()
 {
   unset($this->widgetSchema['content'],$this->validatorSchema['content']);
   $this->widgetSchema   ['content_file'] = new sfWidgetFormInputFile();
   $this->validatorSchema['content_file'] = new sfValidatorFile(array(
     'mime_types' => array('image/gif', 'image/jpg', 'image/png', 'image/jpeg'),
   ));
   $this->validatorSchema['type']->setOption('required',false);
   $this->validatorSchema['name']->setOption('required',false);
 }
 
 public function doSave($con = NULL)
 {
   $this->translateValues();
   return parent::doSave($con);
 }
 
 // transforming the sfValidatedFile into Picture's properties
 public function translateValues()
 {
   $this->values['content']  = base64_encode(file_get_contents($this->values['content_file']->getTempName()));
   $this->values['name']     = $this->values['content_file']->getOriginalName();
   $this->values['type']     = $this->values['content_file']->getType();
   unset($this->values['content_file']);
 }
}

Nous testons ce formulaire dans un module ./symfony doctrine:generate-admin XXX Picture et tout fonctionne très bien. Passons maintenant à sa version "embarquée" dans le sfFormDoctrine du modèle Group :

// lib/forms/doctrine/GroupForm.class.php
class GroupForm extends BaseGroupForm
{
  // ...
  public function doSave($con = NULL)
  {
    $picform_name = 'Picture';
    $file = $this->values[$picform_name]['content_file'];
    unset($this->values[$picform_name]['content_file']);
    
    if (!( $file instanceof sfValidatedFile ))
      unset($this->embeddedForms[$picform_name]);
    else
    {
      // data translation
      $this->values[$picform_name]['content']  = base64_encode(file_get_contents($file->getTempName()));
      $this->values[$picform_name]['name']     = $file->getOriginalName();
      $this->values[$picform_name]['type']     = $file->getType();
      $this->values[$picform_name]['width']    = 24;
      $this->values[$picform_name]['height']   = 16;
    }
    
    return parent::doSave($con);
  }
  
  // ...
  
  public function configure()
  {
    // ...
    $this->embedRelation('Picture');
    foreach ( array('name', 'type', 'version', 'height', 'width',) as $fieldName )
      unset($this->widgetSchema['Picture'][$fieldName], $this->validatorSchema['Picture'][$fieldName]);
    $this->validatorSchema['Picture']['content_file']->setOption('required',false);
    // ...
  }
}

Puisque tous les champs sont définis par un fichier à télécharger, nous procédons à un unset des champs inutiles. Parce qu'un Group peut avoir ou non une Picture, on précise que le fichier téléchargé n'est pas requis. La fonction doSave() surchargée permet d'utiliser, comme dans le PictureForm le fichier uploadé comme point de départ pour les données à enregistrer en base de données.

Si on essaie le formulaire en l'état on s'aperçoit :

  • La Picture est bien créée
  • Le groupe est bien sauvegardé
  • Le lien entre les deux est perdu

Solutionnons tout cela ...

one-to-many, avec des images et des groupes

Le problème est tout simple :

  • apparemment, la méthode sfFormDoctrine::embedRelation() considère les relations dans le sens PictureGroup et non l'inverse ; autrement dit de l'objet unique vers ses multiples objets liés, disposant donc de la clé étrangère référençant l'objet initial.
  • l'objet Group dont le formulaire courant est issu a une propriété picture_id qui vient écraser le lien qui est fait par le $this->embedRelation('Picture');
  • nous allons donc supprimer les widget et validator picture_id du formulaire, pour laisser seule la méthode sfFormDoctrine::embedRelation() gérer ce lien
// lib/forms/doctrine/GroupForm.class.php
class GroupForm extends BaseGroupForm
{
  // ...
  public function configure()
  {
    // ...
    $this->embedRelation('Picture');
    foreach ( array('name', 'type', 'version', 'height', 'width',) as $fieldName )
      unset($this->widgetSchema['Picture'][$fieldName], $this->validatorSchema['Picture'][$fieldName]);
    $this->validatorSchema['Picture']['content_file']->setOption('required',false);
    unset($this->widgetSchema['picture_id'], $this->validatorSchema['picture_id']); // RETRAIT DES WIDGET ET VALIDATOR
    // ...
  }
}

Et le tour est joué...