Bonjour amis développeurs !

Je suis grand débutant sur symfony. Je suis un tutoriel d'OpenClassroom pour apprendre tout en me guidant dans mon projet, et cela fait plusieurs jours que je tourne en rond autour de mon soucis. J'ai eu beau chercher sur beaucoup de forums, mon problème est assez pointus et il m'a été difficile de trouver des topics pouvant m'aider (d'où ce titre de sujet assez peu explicite...). Je suis sûr que la nature de mon problème est bénigne ou tout simplement dûe à une mauvaise compréhension/utilisation du framework, mais toute aide et la bienvenue.

Voici mon problème en tâchant d'être le plus clair possible:

Contexte:
Mon projet est de créer un site pour un groupe d'amis qui proposent des tests et émissions de jeux vidéos sur youtube.

L'erreur:

An exception occurred while executing 'INSERT INTO image (name, extension, videoGame_id) VALUES (?, ?, ?)' with params ["burnoutparadise.jpg", "jpeg", null]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'videoGame_id' cannot be null

Localisé dans le l'action createAction de mon controlleur VideoGameController:

/**
     * Creates a new VideoGame entity.
     *
     * @Route("/", name="videogame_create")
     * @Method("POST")
     * @Template("NSTestsBundle:VideoGame:new.html.twig")
     */
    public function createAction(Request $request)
    {
        $entity = new VideoGame();
        $form = $this->createCreateForm($entity);
        $form->handleRequest($request);

        if ($form->isValid()){
            $em = $this->getDoctrine()->getManager();
            var_dump($entity->getImages());

            // *******   ICI   **********
            $em->persist($entity);

            var_dump($entity->getImages()); die();
            $em->flush();

            return $this->redirect($this->generateUrl('videogame_show', array('id' => $entity->getId())));
        }

        return array(
            'entity' => $entity,
            'form'   => $form->createView(),
        );
    }

Avant la persistance de l'objet image, var_dump($entity->getImages()); me donne:

object(Doctrine\Common\Collections\ArrayCollection)[301]
  private '_elements' => 
    array (size=1)
      0 => 
        object(NS\HomeBundle\Entity\Image)[1051]
          private 'videoGame' => null
          private 'id' => null
          private 'name' => string 'burnout' (length=7)
          private 'extension' => null
          private 'file' => 
            object(Symfony\Component\HttpFoundation\File\UploadedFile)[13]
              ...
          private 'tempFilename' => null

Après persistance:

object(Doctrine\Common\Collections\ArrayCollection)[301]
  private '_elements' => 
    array (size=1)
      0 => 
        object(NS\HomeBundle\Entity\Image)[1051]
          private 'videoGame' => null
          private 'id' => null
          private 'name' => string 'burnoutparadise.jpg' (length=19)
          private 'extension' => string 'jpeg' (length=4)
          private 'file' => 
            object(Symfony\Component\HttpFoundation\File\UploadedFile)[13]
              ...
          private 'tempFilename' => null

Pour résumer:
Mon objet Image imbriqué dans l'objet Videogame ne récupère pas l'ID de VideoGame du coup j'ai une erreur sql lors de la persistance...

But de ma manoeuvre:
Persister une entité VideoGame possédant plusieurs jointures, en particulier une jointure OneToMany avec plusieurs entités Image.
L'entité VideoGame:

<?php
namespace NS\TestsBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * VideoGame
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="NS\TestsBundle\Entity\VideoGameRepository")
 */
class VideoGame
{
    /**
     * @ORM\OneToMany(targetEntity="NS\TestsBundle\Entity\Test", mappedBy="videoGame", cascade={"persist"})
     */
    private $tests;

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date_release", type="datetime")
     */
    private $dateRelease;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date_creation", type="datetime")
     */
    private $dateCreation;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date_edit", type="datetime", nullable=true)
     */
    private $dateEdit;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, unique=true)
     */
    private $name;

    /**
     * @ORM\ManyToMany(targetEntity="NS\TestsBundle\Entity\Genre", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $genres;

    /**
     * @ORM\ManyToOne(targetEntity="NS\TestsBundle\Entity\Engine", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $engine;

    /**
     * @ORM\ManyToMany(targetEntity="NS\TestsBundle\Entity\Platform", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $platforms;

    /**
     * @ORM\ManyToMany(targetEntity="NS\TestsBundle\Entity\Serie", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $series;

    /**
     * @ORM\OneToMany(targetEntity="NS\HomeBundle\Entity\Image", mappedBy="videoGame", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $images;

    /**
     * @ORM\ManyToMany(targetEntity="NS\TestsBundle\Entity\Studio", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $studios;

    /**
     * @ORM\ManyToOne(targetEntity="NS\TestsBundle\Entity\Publisher", inversedBy="videoGames", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $publisher;

    public function __construct()
    {
        $this->dateRelease = new \Datetime();
        $this->dateCreation = new \Datetime();
        $this->dateEdit = NULL;
        $this->genres = new ArrayCollection();
        $this->platforms = new ArrayCollection();
        $this->series = new ArrayCollection();
        $this->images = new ArrayCollection();
        $this->studios = new ArrayCollection();
    }

    public function __toString()
    {
        return $this->name;
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set dateRelease
     *
     * @param \DateTime $dateRelease
     * @return \DateTime
     */
    public function setDateRelease($dateRelease)
    {
        $this->dateRelease = $dateRelease;

        return $this;
    }

    /**
     * Get dateRelease
     *
     * @return \DateTime 
     */
    public function getDateRelease()
    {
        return $this->dateRelease;
    }

    /**
     * Set dateCreation
     *
     * @param \DateTime $dateCreation
     * @return Test
     */
    public function setDateCreation($dateCreation)
    {
        $this->dateCreation = $dateCreation;

        return $this;
    }

    /**
     * Get dateCreation
     *
     * @return \DateTime 
     */
    public function getDateCreation()
    {
        return $this->dateCreation;
    }

    /**
     * Set dateEdit
     *
     * @param \DateTime $dateEdit
     * @return Test
     */
    public function setDateEdit($dateEdit)
    {
        $this->dateEdit = $dateEdit;

        return $this;
    }

    /**
     * Get dateEdit
     *
     * @return \DateTime 
     */
    public function getDateEdit()
    {
        return $this->dateEdit;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return string
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    public function addGenre(Genre $genre)
    {
        $this->genres[] = $genre;
        $genre->addVideoGame($this);

        return $this;
    }

    public function removeGenre(Genre $genre)
    {
        $this->genres->removeElement($genre);
    }

    public function getGenres()
    {
        return $this->genres;
    }

    /**
     * Set engine
     *
     * @param Engine $engine
     * @return Engine
     */
    public function setEngine(Engine $engine)
    {
        $this->engine = $engine;

        return $this;
    }

    /**
     * Get engine
     *
     * @return Engine 
     */
    public function getEngine()
    {
        return $this->engine;
    }

    /**
     * Set publisher
     *
     * @param Publisher $publisher
     * @return Publisher
     */
    public function setPublisher(Publisher $publisher)
    {
        $this->publisher = $publisher;

        return $this;
    }

    /**
     * Get publisher
     *
     * @return Publisher 
     */
    public function getPublisher()
    {
        return $this->publisher;
    }

    public function addPlatform(Platform $platform)
    {
        $this->platforms[] = $platform;
        $platform->addVideoGame($this);

        return $this;
    }

    public function removePlatform(Platform $platform)
    {
        $this->platforms->removeElement($platform);
    }

    public function getPlatforms()
    {
        return $this->platforms;
    }

    public function addSerie(Serie $serie)
    {
        $this->series[] = $serie;
        $serie->addVideoGame($this);

        return $this;
    }

    public function removeSerie(Serie $serie)
    {
        $this->series->removeElement($serie);
    }

    public function getSeries()
    {
        return $this->series;
    }

    public function addImage(Image $image)
    {
        $image->setVideoGame($this);
        $this->images[] = $image;

        return $this;
    }

    public function removeImage(Image $image)
    {
        $this->images->removeElement($image);
    }

    public function getImages()
    {
        return $this->images;
    }

    public function addStudio(Studio $studio)
    {
        $this->studios[] = $studio;
        $studio->addVideoGame($this);

        return $this;
    }

    public function removeStudio(Studio $studio)
    {
        $this->studios->removeElement($studio);
    }

    public function getStudios()
    {
        return $this->studios;
    }
}

L'entité Image:

<?php

namespace NS\HomeBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * Image
 *
 * @ORM\Table(name="image")
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Image
{

    /**
     * @ORM\ManyToOne(targetEntity="NS\TestsBundle\Entity\VideoGame", inversedBy="images")
     * @ORM\JoinColumn(nullable=false)
     */
    private $videoGame;

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=50, nullable=false)
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(name="extension", type="string", length=255, nullable=false)
     */
    private $extension;

    private $file;

    private $tempFilename;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Country
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set extension
     *
     * @param string $extension
     * @return Country
     */
    public function setExtension($extension)
    {
        $this->extension = $extension;

        return $this;
    }

    /**
     * Get extension
     *
     * @return string 
     */
    public function getExtension()
    {
        return $this->extension;
    }

    public function getFile()
    {
        return $this->file;
    }

  // On ajoute cet attribut pour y stocker le nom du fichier temporairement

  // On modifie le setter de File, pour prendre en compte l'upload d'un fichier lorsqu'il en existe déjà un autre
  public function setFile(UploadedFile $file)
  {
    $this->file = $file;

    // On vérifie si on avait déjà un fichier pour cette entité
    if (null !== $this->extension) {
      // On sauvegarde l'extension du fichier pour le supprimer plus tard
      $this->tempFilename = $this->extension;

      // On réinitialise les valeurs des attributs extension et name
      $this->extension = null;
      $this->name = null;
    }
  }

  /**
   * @ORM\PrePersist()
   * @ORM\PreUpdate()
   */
  public function preUpload()
  {
    // Si jamais il n'y a pas de fichier (champ facultatif)
    if (null === $this->file) {
      return;
    }

    $this->extension = $this->file->guessExtension();

    $this->name = $this->file->getClientOriginalName();
  }

  /**
   * @ORM\PostPersist()
   * @ORM\PostUpdate()
   */
  public function upload()
  {
    // Si jamais il n'y a pas de fichier (champ facultatif)
    if (null === $this->file) {
      return;
    }

    // Si on avait un ancien fichier, on le supprime
    if (null !== $this->tempFilename) {
      $oldFile = $this->getUploadRootDir().'/'.$this->id.'.'.$this->tempFilename;
      if (file_exists($oldFile)) {
        unlink($oldFile);
      }
    }

    // On déplace le fichier envoyé dans le répertoire de notre choix
    $this->file->move(
      $this->getUploadRootDir(), // Le répertoire de destination
      $this->id.'.'.$this->extension   // Le nom du fichier à créer, ici « id.extension »
    );
  }

  /**
   * @ORM\PreRemove()
   */
  public function preRemoveUpload()
  {
    // On sauvegarde temporairement le nom du fichier, car il dépend de l'id
    $this->tempFilename = $this->getUploadRootDir().'/'.$this->id.'.'.$this->extension;
  }

  /**
   * @ORM\PostRemove()
   */
  public function removeUpload()
  {
    // En PostRemove, on n'a pas accès à l'id, on utilise notre nom sauvegardé
    if (file_exists($this->tempFilename)) {
      // On supprime le fichier
      unlink($this->tempFilename);
    }
  }

  public function getUploadDir()
  {
    // On retourne le chemin relatif vers l'image pour un navigateur
    return 'uploads/img/videogame';
  }

  protected function getUploadRootDir()
  {
    // On retourne le chemin relatif vers l'image pour notre code PHP
    return __DIR__.'/../../../../web/'.$this->getUploadDir();
  }

    /**
     * Set videoGame
     *
     * @param VideoGame $videoGame
     * @return Image
     */
    public function setVideoGame(VideoGame $videoGame)
    {
        $this->videoGame = $videoGame;

        return $this;
    }

    /**
     * Get videoGame
     *
     * @return VideoGame 
     */
    public function getVideoGame()
    {
        return $this->videoGame;
    }
}

Méthodes utilisées:
Je construis mes formulaires à l'aide de doctrine. J'ai construis mes vues et mon controlleur avec la génération de CRUD doctrine. Comme je souhaite pouvoir joindre plusieurs images à un jeu vidéo j'utilise l'option collection et add_allow dans mon VideoGameType.php . Et je génère des inputs de type file par javascript.

<?php

namespace NS\TestsBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use NS\HomeBundle\Form\ImageType;

class VideoGameType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('dateRelease',    'date', array(
                'format' => 'ddMMyyyy',
                'years' => range(intval(date('Y', strtotime('now +10 years'))), 1970)
            ))
            ->add('name',           'text')
            ->add('genres',         'entity', array(
                'class'    => 'NSTestsBundle:Genre',
                'property' => 'name',
                'multiple' => true
            ))
            ->add('engine',         'entity', array(
                'class'    => 'NSTestsBundle:Engine',
                'property' => 'name'
            ))
            ->add('publisher',         'entity', array(
                'class'    => 'NSTestsBundle:Publisher',
                'property' => 'name'
            ))
            ->add('studios',       'entity', array(
                'class'    => 'NSTestsBundle:Studio',
                'property' => 'name',
                'multiple' => true
            ))
            ->add('platforms',       'entity', array(
                'class'    => 'NSTestsBundle:Platform',
                'property' => 'name',
                'multiple' => true
            ))
            ->add('series',         'entity', array(
                'class'    => 'NSTestsBundle:Serie',
                'property' => 'name',
                'multiple' => true
            ))
            ->add('images',         'collection', array(
                'type'         => new ImageType(),
                'allow_add'    => true,
                'allow_delete' => true
            ))
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'NS\TestsBundle\Entity\VideoGame'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'ns_testsbundle_videogame';
    }
}

Merci d'avance pour votre aide et votre temps.

19 réponses


Salut,

Es tu obligé d'avoir une relation bi-directionnelle entre image et videogames?
Si non essayes pour voir.

Bonjour Leenzur,
je pense qu'il y a un problème au niveau de ta syntaxe.
$entity = new VideoGame();
$form = $this->createForm(new VideoGameType(), $entity);
$form->handleRequest($request);
// Si tu soumets le formulaire
if( $request->getMethod() === 'POST') {
if ($form->isValid()){
$em = $this->getDoctrine()->getManager();
var_dump($entity->getImages());
...
}

Je ne sais pas trop, mais quand tu dis entité propriétaire, il me semble que c'est l'entité qui contient manyToOne qu'il est, donc ton entité "Image", en plus dans ImageEntity il n'y a pas de cascade=persist pour l'entité videoGame, il faut donc que tu rajoutes le lien explicitement dans ton controller, genre $entity->getImage()->setVideoGame($entity).

Il y a aussi un autre problème, concernant l'enregistremenent de fichiers multiples, la fonction ne marche qu'à partir de la version 2.5 il me semble, je t'envoie sur un lien peut être ça pourra t'aider. http://stackoverflow.com/questions/6736946/multiple-file-upload-with-symfony2.

Leenzur
Auteur

Salut Gregory ! Merci pour ta réponse.
J'y ai pensé aussi. Mais comme j'ai une relation VideoGame OneToMany Image, l'entité propriétaire est Image. Donc (si j'ai bien compris), si je souhaite lister les images liées au jeu sur la fiche du jeu en question je suis obligé de faire une relation bidirectionnelle non ?
Merci pour ton aide

Leenzur
Auteur

Merci pour ta réponse Julio,
oui en effet j'avais tenté plusieurs choses concernant les persist sur $videoGame et $images, j'ai mal copié le code. Ma version actuelle contient bien le cascade={"persist"} mais rien y fait...
Merci pour ton lien je vais voir ça tout de suite =)

Leenzur
Auteur

Mais normalement la fonction persist() devrait bien exécuter:

public function addImage(Image $image)
    {
        $this->images[] = $image;
        $image->setVideoGame($this);

        return $this;
    }

non ?
Je n'arrive pas à voir mon erreur... =/

Leenzur
Auteur

Je viens de faire un tour vers ton lien Julio, en fait j'ai la même erreur avec une image comme plusieurs donc je ne pense pas que ça vient de là même s'il est possible que ce soit la prochaine erreur que je rencontre ^^ c'est toujours bon à savoir !

en fait ce qu'il faut que tu saches puisque tu utilises une relation bidirectionnelle il faut que les deux entités soient au courant l'une de l'autre.
donc si tu enregistres $videoGame->setImage() il faut que l'entité Image connaisse videoGame , $image->setvideogame(), regarde le tuto de Openclassroom il donne un exemple de ce cas de figure. J'espère que ça pourra t'aider. en plus je ne sais pas si le format date est correct dans ton formtype, il me semble que c'est dd/MM/yyyy.

Leenzur
Auteur

En me concentrant sur l'essentiel,
côté VideoGame.php j'ai:

    use Doctrine\Common\Collections\ArrayCollection;

    ...

    /**
     * @ORM\OneToMany(targetEntity="NS\HomeBundle\Entity\Image", mappedBy="videoGame", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=false)
     */
     private $images;

    ...

    public function __construct()
    {
        $this->images = new ArrayCollection();
    }

    public function addImage(Image $image)
    {
                                // en faisant ceci VideoGame connait Image
        $this->images[] = $image;
                                // en faisant Image connait VideoGame
        $image->setVideoGame($this);

        return $this;
    }

et côté Image.php j'ai:

    /**
     * @ORM\ManyToOne(targetEntity="NS\TestsBundle\Entity\VideoGame", inversedBy="images", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $videoGame;

    ...

    /**
     * Set videoGame
     *
     * @param VideoGame $videoGame
     * @return Image
     */
    public function setVideoGame(VideoGame $videoGame)
    {
        $this->videoGame = $videoGame;

        return $this;
    }

Donc normalement les deux entités se connaissent. J'ai même changer l'ordre d'appel des fonctions en :

    public function addImage(Image $image)
    {
        $image->setVideoGame($this);    // ligne échangée avec...
        $this->images[] = $image;               // ... cette ligne

        return $this;
    }

Mais rien ça ne change rien.. :/

Leenzur
Auteur

Sinon concernant le format dans FormType, le format est bon. Enlever les "/" me permet juste de ne pas les afficher dans la vue ^^

dans la fonction addImage() il faut modifier $ this-> images-> setvideoGame ($ this) et non pas $ image-> setVideoGame

Leenzur
Auteur

Je viens d'essayer. Toujours aucun changement. En fait j'ai placé un die() dans la methode addImage() et le script ne s'est pas arrêté...

En fait persist() ne fait pas appel à cette méthode alors qu'il fait bien appel aux autres. Je ne comprends pas pourquoi... =(

Faire $ this-> images-> setvideoGame ($ this) me paraît étrange vu que images est un tableau d'images et non un objet.

il me semble que c'est faisable car avec cette syntaxe on applique la méthode set sur le dernier élément ajouté à la liste, qui est un objet de type Image.

Leenzur
Auteur

Ce serait une fonctionnalité de ArrayCollection ?
Quoi qu'il en soit je n'ai toujours pas compris pourquoi addImage() est bien appelé lors du persist des autres entité mais pas dans ce cas de OneToMany =/

Je pense qu'il y a truc à faire au niveau du controller. il me semble que dans l'exemple de OpenClassRoom il appelle la méthode add dans le controller, car elle n'est pas l'entité propriétaire, regarde de ce côté.

Leenzur
Auteur

Bon... Je viens de faire un ManyToMany à la place d'un OneToMany et ça fonctionne... Donc tant pis pour la rigueur car j'avoue être bien lassé de cette erreur.
Du coup je ne sais pas si je peux marquer ce sujet comme résolu.
Merci pour ton aide Julio en tout cas !

Leenzur
Auteur

Car sur trois forums différents tu es bien le seul à m'avoir épaulé.

désolé de ne pas avoir pu t'aider à résoudre ton problème, mais je suis sûr que tu trouveras, il suffit de l'oublier un peu de temps et d'y revenir après. En tous cas, c'est bien que tu te sois rapatrié sur une autre solution. Bon courage pour la suite de ton projet.

Leenzur
Auteur

Merci Julio toi aussi bonne continuation ;)

Salut,
Je déterre un peu ce sujet, car j'ai eut un problème similaire et je suis tombé sur ce topic.
La solution (qui a fonctionnée pour moi) ça serait ça
symfony-form-and-doctrine-inverse-side-association
Fallait le savoir.
En éspèrant que ça vous aide.