Bonjour,
Je suis entrain de réaliser un petit blog sous Symfony2 , j'ai un formulaire Article avec un autre formulaire imbriqué Image , je me suis retrouvé face à un problème au niveau de la base de données , ainsi lorsque je remplie les champs du formulaire , au niveau de la base tout est bon sauf les champs image de la table Articles et le champs article_id de la table Images .
Alors je sais pas si l'erreur est au niveau du code ou bien c'est un problème d'association entre les tables .

Le code de mon application :

<?php
namespace Blog\newsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
 * Article
 *
 * @ORM\Table(name="articles")
 * @ORM\Entity(repositoryClass="Blog\newsBundle\Entity\ArticleRepository")
 */
class Article
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @var string
     *
     * @ORM\Column(name="titre", type="string", length=255)
     */
    private $titre;
    /**
     * @var \DateTime
     *
     * @ORM\Column(name="datePub", type="datetime")
     */
    private $datePub;
    /**
     * @var string
     *
     * @ORM\Column(name="contenue", type="text")
     */
    private $contenue;
    /**
     * @var string
     *
     * @ORM\Column(name="auteur", type="string", length=255)
     */
    private $auteur;
    /**
     * @ORM\Column(name="image" ,type="integer")
     * @ORM\OneToOne(targetEntity="Image" , mappedBy="article" )
     */
    private $image;
    /**
     * @var boolean
     *
     * @ORM\Column(name="publication", type="boolean")
     */
    private $publication;
    function __construct()
    {
        $this->publication = true;
        $this->datePub = new \DateTime();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    /**
     * Set titre
     *
     * @param string $titre
     * @return Article
     */
    public function setTitre($titre)
    {
        $this->titre = $titre;
        return $this;
    }
    /**
     * Get titre
     *
     * @return string
     */
    public function getTitre()
    {
        return $this->titre;
    }
    /**
     * Set datePub
     *
     * @param \DateTime $datePub
     * @return Article
     */
    public function setDatePub($datePub)
    {
        $this->datePub = $datePub;
        return $this;
    }
    /**
     * Get datePub
     *
     * @return \DateTime
     */
    public function getDatePub()
    {
        return $this->datePub;
    }
    /**
     * Set contenue
     *
     * @param string $contenue
     * @return Article
     */
    public function setContenue($contenue)
    {
        $this->contenue = $contenue;
        return $this;
    }
    /**
     * Get contenue
     *
     * @return string
     */
    public function getContenue()
    {
        return $this->contenue;
    }
    /**
     * Set auteur
     *
     * @param string $auteur
     * @return Article
     */
    public function setAuteur($auteur)
    {
        $this->auteur = $auteur;
        return $this;
    }
    /**
     * Get auteur
     *
     * @return string
     */
    public function getAuteur()
    {
        return $this->auteur;
    }
    /**
     * Set publication
     *
     * @param boolean $publication
     * @return Article
     */
    public function setPublication($publication)
    {
        $this->publication = $publication;
        return $this;
    }
    /**
     * Get publication
     *
     * @return boolean
     */
    public function getPublication()
    {
        return $this->publication;
    }
    /**
     * Set image
     *
     * @param integer $image
     * @return Article
     */
    public function setImage($image)
    {
        $this->image = $image;
        return $this;
    }
    /**
     * Get image
     *
     * @return integer
     */
    public function getImage()
    {
        return $this->image;
    }
}

<?php
namespace Blog\newsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
 *
 * @ORM\Table(name="images")
 * @ORM\Entity(repositoryClass="Blog\newsBundle\Entity\ImageRepository")
 */
class Image
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     *@ORM\OneToOne(targetEntity="Article" , inversedBy="image")
     *@ORM\JoinColumn(name="article_id" , referencedColumnName="id")
     */
    private $article;
    /**
     * @var string
     *
     * @ORM\Column(name="url", type="string", length=255)
     */
    private $url;
    /**
     * @var string
     *
     * @ORM\Column(name="alt", type="string", length=255)
     */
    private $alt;

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

    /**
     * Set url
     *
     * @param string $url
     * @return Image
     */
    public function setUrl($url)
    {
        $this->url = $url;
        return $this;
    }
    /**
     * Get url
     *
     * @return string
     */
    public function getUrl()
    {
        return $this->url;
    }
    /**
     * Set alt
     *
     * @param string $alt
     * @return Image
     */
    public function setAlt($alt)
    {
        $this->alt = $alt;
        return $this;
    }
    /**
     * Get alt
     *
     * @return string
     */
    public function getAlt()
    {
        return $this->alt;
    }
    /**
     * Set article
     *
     * @param \Blog\newsBundle\Entity\Article $article
     * @return Image
     */
    public function setArticle(\Blog\newsBundle\Entity\Article $article = null)
    {
        $this->article = $article;
        return $this;
    }
    /**
     * Get article
     *
     * @return \Blog\newsBundle\Entity\Article
     */
    public function getArticle()
    {
        return $this->article;
    }
    public function __toString()
    {
        return strval($this->id);
    }
}

<?php
namespace Blog\newsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Blog\newsBundle\Form\ImageType;
class ArticleType extends AbstractType
{
        /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('titre','text')
            ->add('datePub','date')
            ->add('contenue','textarea')
            ->add('auteur','text')
            ->add('publication','checkbox',array('required'=>false))
            ->add('image',new ImageType(),array("by_reference" =>false))
            ->add('Submit','submit')
            ->add('Reset','reset')
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Blog\newsBundle\Entity\Article'
        ));
    }
    /**
     * @return string
     */
    public function getName()
    {
        return 'blog_newsbundle_articletype';
    }
}

<?php
namespace Blog\newsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ImageType extends AbstractType
{
        /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('url','text')
            ->add('alt','text')
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Blog\newsBundle\Entity\Image'
        ));
    }
    /**
     * @return string
     */
    public function getName()
    {
        return 'blog_newsbundle_imagetype';
    }
}

Le controlleur

<?php
namespace Blog\newsBundle\Controller;
use Blog\newsBundle\Entity\Article;
use Blog\newsBundle\Form\ArticleType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
    public function indexAction($name)
    {
        return $this->render('BlognewsBundle:Default:index.html.twig', array('name' => $name));
    }
    public function ajouterAction(Request $request)
    {
        $article=new Article();
        $formBuilder=$this->createFormBuilder($article);
        $formBuilder->add('titre','text')
                    ->add('datePub','date')
                    ->add('auteur','text')
                    ->add('contenue','textarea')
                    ->add('publication','checkbox',array('required'=>false))
                    ->add('Envoyer','submit')
                    ->add('Cancel','reset');
        //A partir du formBuilder on genere le formulaire
        $form=$formBuilder->getForm();
        $request=$this->get('request');
        if($request->getMethod()=="POST")
        {
            $form->bind($request);
            if($form->isValid())
            {
               $em=$this->getDoctrine()->getManager();
                $em->persist($article);
                $em->flush();
                return $this->redirect($this->generateUrl("blognews_afficher",array("id"=>$article->getId())));
            }
        }
        return $this->render("BlognewsBundle:Default:ajouter.html.twig",array('form'=>$form->createView()));
    }
    public function afficherAction($id)
    {
        $em=$this->getDoctrine()->getRepository('Blog\newsBundle\Entity\Article');
        $article=$em->find($id);
        return $this->render('BlognewsBundle:Default:afficher.html.twig',array('article'=>$article));
    }

    public function ajouter2Action()
    {
        $article=new Article();
        $form=$this->createForm(new ArticleType(),$article);
        $request=$this->get('request');
        if($request->getMethod()=='POST')
        {
            $form->bind($request);
            if($form->isValid())
            {
                $em=$this->getDoctrine()->getManager();
                $em->persist($article);
                $em->persist($article->getImage());
                $em->flush();
                return $this->redirect($this->generateUrl('blognews_afficher',array('id'=>$article->getId())));
            }
        }
        return $this->render("BlognewsBundle:Default:ajouter.html.twig",array('form'=>$form->createView()));
    }
}

<form action="{{ path("blognews_ajouter2") }}" method="post" {{ form_enctype(form)}}>
    {{ form_errors(form) }}
    <div>
        {{ form_errors(form.titre) }}
        {{ form_label(form.titre,"Titre de l'article ") }}
        {{ form_widget(form.titre) }}
    </div>
    <div>
        {{ form_errors(form.contenue) }}
        {{ form_label(form.contenue,"Contenue de l'article")}}
        {{ form_widget(form.contenue)}}
    </div>
    {{ form_rest(form) }}
</form>

15 réponses


Je me plante ou a aucun moment tu ne persistes image ?
Essaye de modifier le mapping ORM d'Image, dans Article, en ajoutant cascade=persiste:

/**
* @ORM\Column(name="image" ,type="integer")
* @ORM\OneToOne(targetEntity="Image" , mappedBy="article", cascade={"persist"})
*/
private $image;

Ou fais ton persiste toi-meme d'image (la premiere solution est meilleure)

ayoub246
Auteur

Merci pour votre réponse mais je précise que je persiste image dans le controleur grace à travers cette ligne

$em->persist($article->getImage());

et j'ai essayé avec cascade={"persist"} mais j'ai toujours le champs article_id de la table images remplie avec NULL

Ah bah oui, tiens ... je suis con :/ ...

Essaye d'ajouter

use Blog\newsBundle\Entity\Image;
use Blog\newsBundle\Form\ImageType;

a ton controller (a adapter en fonction de ta config), et éventuellemt force le type de l'attribut image de ta class Article en mettant

// Ta classe article
    public function setImage(\Blog\newsBundle\Entity\Image $image)
    {
        $this->image = $image;

        return $this;
    }
ayoub246
Auteur

Merci encore une fois , mais malgré ces changements le champs article_id de la table Images est à NULL .

T'as essayé sans mettre le

array("by_reference" =>false)

dans ton ArticleType?

ayoub246
Auteur

Oui j'ai essayé et ça marche pas !
Je vais essayer de refaire le tout des le début un autre jour , parce que là j'ai passé tout la journée à chercher , et j'ai beaucoup de chose à apprendre dans Symfony2 vu que je suis un débutant .
Je tiens à vous remercier pour l'effort fourni .

Dsl de pas etre plus utile. Si jamais tu trouves ce qui coince poste-le bien ici et valide ta réponse!
Bon courage ;)

Bonsoir, je suis sur un tout autre projet mais j'ai exactement le même problème similaire avec des formulaires imbriqués.
Le champ airport_id faisant référence à l'ID de la première entité créée est vide et je me retrouve avec un champ NULL dans la table.
Si ayoub246 trouve la solution, ça m'intéresse !

Stéphane

Je me demande comment vous vous débrouillez les gars ^^ !
J'ai fais un formulaire imbriqué a la con hier encore pour vérifier et ca marche sans soucis. Utilises-tu la console pour générer tes entités ? tes form ? ton CRUD ?

Peux-tu poster le controller qui affiche le formulaire et qui le valide, stp ? ainsi que tes entités et tes form ?

ayoub246
Auteur

Bonjour , non toujours pas de solution calimhiro , pour ma part je génére mes entités et mes forms en utilisant la console , pour le contrôleur je le code moi même !

A toutes fins utiles, voici une procédure qui fonctionne pour générer

  • Un bundle Test pour faire le test
  • Une entité Commentaire très simple (titre et auteur uniquement)
  • Une entité Author très simple (nom uniquement)
  • Faire tout le mapping ORM et générer les formulaire ainsi que le CRUD de commentaire.
  • l'auteur du commentaire est bien entendu une instance de l'entité Author

Je suggère que vous essayiez et que vous comparies avec vos trucs:

----------- (I) GENERATION DU BUNDLE -----------

app/console generate:bundle
    Bundle namespace: Test/TestBundle
    Bundle name: TestTestBundle
    directory: web/src/
    Configuration: yml
    Whole structre: yes
    Confirm: yes
    Add to kernl: yes
    Add to routing: yes

----------- (II) GENERATION DES ENTITIES -----------

app/console doctrine:generate:entity
    Entity shortcut name: TestTestBundle:Comment
    Mapping onformation: Annotation
    New field name: title
        Field type: String
        Field length: 255
    New field name: author
        Field type: Object
    Empty repo: No
    Confirm: Yes  
//------------------------------------------------
app/console doctrine:generate:entity
    Entity shortcut name: TestTestBundle:Author2 //j'avais d13ja une table author dans ma bdd, c'est pour ca que j'utilise author2 ici
    Mapping information: Annotation
    New field name: name
        Field type: String
        Field length: 255
    Empty repo: No
    Confirm: Yes

----------- (III) OneToOne mapping -----------
Dans Entity/Comment:

//Remplacer:
@ORM\Column(name="author", type="object")
//par:
@ORM\OneToOne(targetEntity="Test\TestBundle\Entity\Author2", cascade={"persist"})

----------- (IV) UPDATES DB -----------

app/console doctrine:schema:update --force

----------- (V) GENERATES FORMS -----------

app/console doctrine:generate:form TestTestBundle:Comment
app/console doctrine:generate:form TestTestBundle:Author2

----------- (VI) GENERATES COMMENT CRUD -----------

app/console doctrine:generate:crud
    Entity name: TestTestBundle:Comment
    Generate write actions yes
    Config: yml
    Routes prefix: /comment
    Confirm: yes
// Import des routes a faire a la main en ce qui me concerne dans Test/TestBundle/Resources/config/routing.yml:
TestTestBundle_comment:
            resource: "@TestTestBundle/Resources/config/routing/comment.yml"
            prefix: /comment

Dans TestBundle/Form/CommentType.php:

//changer
->add('author')
//par
->add('author', new Author2Type())

----------- (VII) AJUSTEMENT DES VUES -----------
Dans TestBundle/Resources/Views/Comment/index.html.twig <u>ET</u> show.html.twig:

//Changer les instances de
{{ entity.author }}
//par:
{{ entity.author.name }}

TOUT ROULE !

@ayoub246: Note que tu gardes ton mapping vers une colonne de la bdd pour ton champ image de article. Le problème peut venir de la.
Essaye de corriger, fait un --dump-sql pour voir s'il y a du changement et force si tu vois que il y a une update a faire ...

ayoub246
Auteur

Bonsoir , Ton code marche à merveille , le mien toujours pas !
Et je crois que tu as raison peut être que le problème vient du mapping du champ image ; je vais essayé de changer le mapping de ton application et le rendre bidirectionnelle pour pouvoir récupérer aussi tous les commentaire d'un auteur , et si ça marche au moins votre effort aurait servi à quelque chose .
Merci Vallyan

<u><strong>OK, seconde partie: mettre en place la bidirectionnalité</strong></u>

Je pars du code généré précédemment, voici ce que j'ai fais pour le modifier:

----------- (I) AJOUT DE L'ATTRIBUT "COMMENT" (+ GETTERS / SETTERS) DANS AUTHOR2.PHP -----------

// Dans Entity/Author2.php
/**
 * @var \stdClass
 */
private $comment;
/**
 * Set Comment
 *
 * @param Comment $comment
 * @return Author2
 */
public function setComment($comment)
{
    $this->comment = $comment;
    return $this;
}
/**
 * Get Comment
 *
 * @return Comment 
 */
public function getComment()
{
    return $this->comment;
}

----------- (II) MAPPING DE "COMMENT::AUTHOR" VERS "AUTHOR2" -----------

// Dans Entity/Comment.php - modifier les annotations de $author
/**
 * @ORM\OneToOne(targetEntity="Test\TestBundle\Entity\Author2", mappedBy="comment", cascade={"persist", "remove"})
 */
private $author

----------- (III) SET DE "COMMENT" DANS "AUTHOR" -----------
Pour chaque nouvelle instance de Comment, on a un Author (= instance de Author2) associé, qui lui-meme possède une instance de Comment (car bidirectionnalité).
Il faut donc préciser que l'instance de Comment de Author2, c'est le Comment courant
Le code suivant est ajouté uniquement dans l'entité propriétaire (ici c'est Comment):

// Dans Entity/Comment.php
public function setAuthor(\Test\TestBundle\Entity\Author2 $author)
{
    $this->author = $author;
    $author->setComment($this); //<- Ajouter ceci: IMPORTANT !!
    return $this;
}

----------- (IV) MAPPING DE "AUTHOR2::COMMENT" VERS "COMMENT" -----------

// Dans Entity/Author2.php, modifier les annotations du $comment qu'on a ajouté plus tot:
/**
 * @var \stdClass
 *
 * @ORM\OneToOne(targetEntity="Test\TestBundle\Entity\Comment", inversedBy="author", cascade={"persist", "remove"})
 */
private $comment;

----------- (V) NE PAS OUBLIER D'UPDATER LA DB -----------

// En ce qui me concerne j'ai d'abord vidé le deux tables avant de faire l'update
app/console doctrine:schema:update --force

C'est fini ! , l'ajout d'un nouveau commentaire (avec un auteur) depuis la page localhost/app_dev.php/comment/new (si vous n'avez pas changé les routes) devrait modifier les tables comments et author2 de la bdd, avec la foreign key mise a jour correctement.

Prochain épisode: récupérer le commentaire a partie d'un auteur

<u>Cette fois on prépare le repository pour récupérer les commentaires des auteurs</u>

----------- (I) AJOUTER UN REPO -----------
Lors de la génération des entités, on n'avait pas ajouté les repo, il faut donc:

  • Créer le fichier Author2Repository et y mettre notre code pour faire notre requete
  • Updater l'entité pour lui dire qu'il y a un repo:

Dans Entity/Author2: modifier le mapping de la class en ajoutant l'adresse du repo:

/**
 * Author2
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Test\TestBundle\Entity\Author2Repository")
 */

Créer le fichier Author2Repository:

<?php
// dans Entity/Author2Repository.php
namespace Test\TestBundle\Entity;
use Doctrine\ORM\EntityRepository;
class Author2Repository extends EntityRepository
{
    // Récupère un array de toutes les instances d'Author2
    public function getAuthorWithComment()
    {
        $qb = $this ->createQueryBuilder('a')
                    ->join('a.comment', 'c')
                    ->addSelect('c');
        return $qb ->getQuery()
                    ->getResult();
    }

    // Récupère une seule instance d'Author2 a partir de son id
    public function getOneAuthorWithComment($id)
    {
        $qb = $this ->createQueryBuilder('a')
                    ->join('a.comment', 'c')
                    ->addSelect('c')
                ->where('a.id = :id')
                    ->setParameter('id', $id);
        return $qb ->getQuery()
                    ->getSingleResult();
    }
}

----------- (II) GENERER LE CONTROLLER -----------
Ici on s’embête pas, on le fait avec la console. On le modifiera ensuite.
ATTENTION: je ne génère que les reads, pas les writes

app/console doctrine:generate:crud
    Entity name: TestTestBundle:Author2
    Generate write actions no // <- ICI ON NE NE DEMANDE QUE LES READS
    Config: yml
    Routes prefix: /author2
    Confirm: yes

// Import des routes a faire a la main en ce qui me concerne dans Test/TestBundle/Resources/config/routing.yml:
TestTestBundle_author2:
            resource: "@TestTestBundle/Resources/config/routing/author2.yml"
            prefix: /author2

On modifie ensuite indexAction et showAction pour qu'ils utilisent nos requete:

// Dans Author2Controller:indexAction
//commenter ou supprimer la ligne:
$entities = $em->getRepository('TestTestBundle:Author2')->findAll();
//et la remplacer par:
$entities = $em->getRepository('TestTestBundle:Author2')->getAuthorWithComment();

// Dans Author2Controller:showAction
//commenter ou supprimer la ligne:
$entities = $em->getRepository('TestTestBundle:Author2')->find($id);
//et la remplacer par:
$entities = $em->getRepository('TestTestBundle:Author2')->getOneAuthorWithComment($id);

----------- (III) MODIFIER LES VUES -----------
Reste plus qu'a modifier les vues pour afficher le commentaire de chaque auteur:

// Dans index.twig.html:
// Ajouter un <th> au tableau après <th>Name</th>
<th>Comment</th>
// Ajouter un <td> après <td>{{ entity.name }}</td>
 <td>{{ entity.comment.title }}</td>

//-------------------------------------------------------
// Dans show.twig.html:
// Ajouter un <tr> pour les comment.title après le <tr> du name
            <tr>
                <th>Comment</th>
                <td>{{ entity.comment.title }}</td>
            </tr>

Voila ! La liste des auteurs est dispo a l'adresse localhost/app_dev.php/author2, et montre le commentaire associé.

Si je résume, pour faire le OneToOne bidirectionnel:

Partir d'un OneToOne unidirectionnel avec une entité propriétaire (comment) et une dépendante (author) Ajouter la bidirectionnalité en modifiant un certain nombre de choses:

-- Ajouter l'attribut comment dans la classe author
-- Ajouter le mappedBy pour Comment::author
-- Ajouter l'hydratation de l'attribut comment de l'instance de la classe author qui correspond a l'instance courante de comment Comment::setAuthor()
-- Ajouter le inversedBy pour Author::comment

Pour accéder a l'entité propriétaire a partir de l'entité inverse, il faut faire un repo custom avec un join, et l'utiliser dans les controller

Bon, j'espère ca aide ... Bonne chance pour identifier les problèmes que vous avez!

ayoub246
Auteur

Merci pour cet effort je vais compléter mon code et voir si un problème apparaît .