Search code examples
phpdoctrineduplicatessymfonytransformer-model

Symfony: Why is persist trying to insert an object retrieved by object manager?


Apologies if this has been asked before. I'm still trying to grasp Symfony terms to my searching isn't that good yet!

I have (for the purpose of this question) two entities: Article and Author.

I'm embedding the Author form into the add Article form. I'd like to check if the email already exists, and, if it does, just update that record (the corresponding name) instead of adding a duplicate. I'm using a transformer to do this.

I am able to find an existing Author in my transformer. However, when I persist the Article in the controller, I get the error:

"An exception occurred while executing 'INSERT INTO author... Integrity constraint violation: 1062 Duplicate entry" ... etc...

I'm really confused because my understanding is that persist should update the record that I just retrieved from the database!

Article Controller:

    public function newAction(Request $request){
    $article = new Article();
    $form = $this->createForm('AppBundle\Form\ArticleType', $article);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {

        //Set the timestamps on article/author.
        $date = new \DateTime("now");
        $article->setCreatedDate($date);
        $article->getAuthor()->setCreatedDate($date);

        //Persist to database.
        $em = $this->getDoctrine()->getManager();
        $em->persist($article);
        $em->flush($article);

        return $this->redirectToRoute('article_show', array('id' => $article->getId()));
    }

    return $this->render('article/new.html.twig', array(
        'article' => $article,
        'form' => $form->createView(),
    ));}

Article Type:

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class ArticleType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name')->add('description')->add('thumbnail');

        //We'll handle dates. Don't want users to access that.
        //$builder->add('createdDate');

        $builder->add('author', AuthorType::class, array("label" => FALSE));
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Article'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_article';
    }


}

Article Entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
/**
 * Article
 *
 * @ORM\Table(name="article")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ArticleRepository")
 */
class Article
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @var string
     *
     * @ORM\Column(name="description", type="text", nullable=true)
     */
    private $description;

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Author", inversedBy="articles", cascade={"persist"})
     * @ORM\JoinColumn(name="author_id", referencedColumnName="id")
     * @Assert\Valid()
     */
    private $author;


    /**
     * @ORM\OneToMany(targetEntity="Review", mappedBy="article")
     */
    private $reviews;

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

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

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

        return $this;
    }

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

    /**
     * Set description
     *
     * @param string $description
     *
     * @return Article
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

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

    /**
     * Set thumbnail
     *
     * @param string $thumbnail
     *
     * @return Article
     */
    public function setThumbnail($thumbnail)
    {
        $this->thumbnail = $thumbnail;

        return $this;
    }

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

    /**
     * Set createdDate
     *
     * @param \DateTime $createdDate
     *
     * @return Article
     */
    public function setCreatedDate($createdDate)
    {
        $this->createdDate = $createdDate;

        return $this;
    }

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

    /**
     * Set authorId
     *
     * @param integer $authorId
     *
     * @return Article
     */
    public function setAuthorId($authorId)
    {
        $this->authorId = $authorId;

        return $this;
    }

    /**
     * Get authorId
     *
     * @return int
     */
    public function getAuthorId()
    {
        return $this->authorId;
    }

    /**
     * Set author
     *
     * @param \AppBundle\Entity\Author $author
     *
     * @return Article
     */
    public function setAuthor(\AppBundle\Entity\Author $author = null)
    {
        $this->author = $author;

        return $this;
    }

    /**
     * Get author
     *
     * @return \AppBundle\Entity\Author
     */
    public function getAuthor()
    {
        return $this->author;
    }

    /**
     * Add review
     *
     * @param \AppBundle\Entity\Review $review
     *
     * @return Article
     */
    public function addReview(\AppBundle\Entity\Review $review)
    {
        $this->reviews[] = $review;

        return $this;
    }

    /**
     * Remove review
     *
     * @param \AppBundle\Entity\Review $review
     */
    public function removeReview(\AppBundle\Entity\Review $review)
    {
        $this->reviews->removeElement($review);
    }

    /**
     * Get reviews
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getReviews()
    {
        return $this->reviews;
    }

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

}

Author Entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * Author
 *
 * @ORM\Table(name="author")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\AuthorRepository")
 * @UniqueEntity(fields={"email"}, message="Note: author already existed. Using that record")
 */
class Author
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

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

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

    /**
     * @ORM\OneToMany(targetEntity="Review", mappedBy="author")
     */
    private $reviews;

    /**
     * @ORM\OneToMany(targetEntity="article", mappedBy="author")
     */
    private $articles;

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


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

    /**
     * Set email
     *
     * @param string $email
     *
     * @return Author
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

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

    /**
     * Set firstName
     *
     * @param string $firstName
     *
     * @return Author
     */
    public function setFirstName($firstName)
    {
        $this->firstName = $firstName;

        return $this;
    }

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

    /**
     * Set lastName
     *
     * @param string $lastName
     *
     * @return Author
     */
    public function setLastName($lastName)
    {
        $this->lastName = $lastName;

        return $this;
    }

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

    /**
     * Set createdDate
     *
     * @param \DateTime $createdDate
     *
     * @return Author
     */
    public function setCreatedDate($createdDate)
    {
        $this->createdDate = $createdDate;

        return $this;
    }

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

    /**
     * Add review
     *
     * @param \AppBundle\Entity\Review $review
     *
     * @return Author
     */
    public function addReview(\AppBundle\Entity\Review $review)
    {
        $this->reviews[] = $review;

        return $this;
    }

    /**
     * Remove review
     *
     * @param \AppBundle\Entity\Review $review
     */
    public function removeReview(\AppBundle\Entity\Review $review)
    {
        $this->reviews->removeElement($review);
    }

    /**
     * Get reviews
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getReviews()
    {
        return $this->reviews;
    }

    /**
     * Add article
     *
     * @param \AppBundle\Entity\article $article
     *
     * @return Author
     */
    public function addarticle(\AppBundle\Entity\article $article)
    {
        $this->articles[] = $article;

        return $this;
    }

    /**
     * Remove article
     *
     * @param \AppBundle\Entity\article $article
     */
    public function removearticle(\AppBundle\Entity\article $article)
    {
        $this->articles->removeElement($article);
    }

    /**
     * Get articles
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getarticles()
    {
        return $this->articles;
    }

    /**
     *Return boolean depending on if the author has already reviewed the article
     * @param \AppBundle\Entity\Author $author
     * @return bool
     */
    public function hasarticle(\AppBundle\Entity\article $article)
    {
        return $this->getarticles()->contains($article);
    }

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

}

Thanks so much in advance for any help you can give. Also, is this the best way to handle duplicate emails? I'd like to update the firstname/lastname as well instead of just letting leaving the existing name in the database. Should I update the name in the controller before persisting or should that somehow be done in the transformer?

Thanks!


Solution

  • Doctrine tries to insert new row into the db because you are calling persist($article). If you meant to update the row then just call flush():

    $this->getDoctrine()->getManager()->flush();
    

    LE

    For this example, I'm using two lighter entities:

    1) AppBundle/Entity/Article.php entity contains only id, name, and author, with:

    /**
     * @ORM\ManyToOne(targetEntity="Author", inversedBy="articles")
     */
    private $author;
    
    public function setAuthor(\AppBundle\Entity\Author $author = null)
    {
        $this->author = $author;
        return $this;
    }
    
    //getAuthor()...
    //getters for id and name + setter for name
    

    2) AppBundle/Entity/Author.php entity contains only id, email, and articles:

    /**
     * @ORM\OneToMany(targetEntity="Article", mappedBy="author")
     */
    private $articles;
    
    //getters for id and email + setter for email
    
    public function __construct()
    {
        $this->articles = new \Doctrine\Common\Collections\ArrayCollection();
    }
    
    public function addArticle(\AppBundle\Entity\Article $article)
    {
        $this->articles[] = $article;
        return $this;
    }
    
    public function removeArticle(\AppBundle\Entity\Article $article)
    {
        $this->articles->removeElement($article);
    }
    
    public function getArticles()
    {
        return $this->articles;
    }
    

    3) AppBundle/Form/ArticleType.php:

    $builder
        ->add('name')
        ->add('author', EntityType::class, [
            'class' => 'AppBundle:Author',
            'placeholder' => ' ',
            'query_builder' => function(EntityRepository $er) {
                return $er->createQueryBuilder('a');
            },
            'choice_label' => function($author) {
                return $author->getEmail();
            },
            'multiple' => false,
            'expanded' => false
        ])
    ;
    

    4) AppBundle/Controller/ArticleController.php

    /**
     * @Route("/{id}/edit", name="article_edit")
     * @Method({"GET", "POST"})
     */
    public function editAction(Request $request, Article $article)
    {
        $form = $this->createForm('AppBundle\Form\ArticleType', $article);
        $form->handleRequest($request);
    
        if ($form->isSubmitted() && $form->isValid()) {
            $this->getDoctrine()->getManager()->flush();
    
            return $this->redirectToRoute('article_edit', array('id' => $article->getId()));
        }
    
        return $this->render('article/edit.html.twig', array(
            'form' => $form->createView(),
        ));
    }
    

    5) app/Resources/views/article/edit.html.twig

    {{ form(form, { attr:{ 'id':'form' }) }}
    <button type="submit" form="form">Update</button>
    

    Basically, this code is generated by the bin/console doctrine:generate:crud AppBundle:Article