Pixelrocker
Pixelrocker

Reputation: 361

Symfony Form: EntityType, Choice Label and related entity

I´ve got an entity News and a related entity NewsArticle which holds the translations of some News attributes (Headline, Subheader, Bodytext). The form to add/edit the News records needs a field to select related News, so I defined a EntityType field for this purpose. The label of each selectable News record should display the headline of the NewsArticle record with a specific field value (the default language). But how can I realise this?

Even my first attempt by using the __toString() method failed with this error message:

Catchable Fatal Error: Object of class Doctrine\ORM\PersistentCollection could not be converted to string

The News entity:

<?php

// src/Acme/Bundle/Entity/News.php
namespace Acme\Bundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity
 * @ORM\Table(name="news")
 */
class News {
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\OneToMany(targetEntity="NewsArticle", mappedBy="news", cascade={"persist", "remove"})
     */
    protected $newsArticle;

    /**
     * @ORM\Column(type="string", length=150, unique=false, nullable=false)
     */
    private $ident;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $validFrom;    

    /**
     * @ORM\Column(type="datetime", nullable=true)
     */
    private $validTo;        

    /**
     * @ORM\OneToMany(targetEntity="NewsImage", mappedBy="news")
     */
    protected $image; 

    /**
     * @ORM\OneToMany(targetEntity="Media", mappedBy="news")
     */
    private $media;

    /**
     * @ORM\ManyToMany(targetEntity="News", inversedBy="relatedTo")
     * @ORM\JoinTable(name="news_to_news",
     *      joinColumns={@ORM\JoinColumn(name="from_news_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="to_news_id", referencedColumnName="id")}
     * )
     */
    private $relatedFrom;     

    /**
     * @ORM\ManyToMany(targetEntity="News", mappedBy="relatedFrom")
     */
    private $relatedTo;           

    /**
     * @ORM\ManyToOne(targetEntity="Artist")
     * @ORM\JoinColumn(name="artist_id", referencedColumnName="id")
     */
    private $artist;

    /**
     * @ORM\Column(type="boolean")
     */
    private $public;

    /**
     * @ORM\Column(type="boolean", nullable=true)
     */
    private $deleted;    

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->newsArticle = new \Doctrine\Common\Collections\ArrayCollection();
        $this->image = new \Doctrine\Common\Collections\ArrayCollection();
        $this->media = new \Doctrine\Common\Collections\ArrayCollection();
        $this->relatedFrom = new \Doctrine\Common\Collections\ArrayCollection();
        $this->relatedTo = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

    /**
     * Set ident
     *
     * @param string $ident
     *
     * @return News
     */
    public function setIdent($ident)
    {
        $this->ident = $ident;

        return $this;
    }

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

    /**
     * Set validFrom
     *
     * @param \DateTime $validFrom
     *
     * @return News
     */
    public function setValidFrom($validFrom)
    {
        $this->validFrom = $validFrom;

        return $this;
    }

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

    /**
     * Set validTo
     *
     * @param \DateTime $validTo
     *
     * @return News
     */
    public function setValidTo($validTo)
    {
        $this->validTo = $validTo;

        return $this;
    }

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

    /**
     * Set public
     *
     * @param boolean $public
     *
     * @return News
     */
    public function setPublic($public)
    {
        $this->public = $public;

        return $this;
    }

    /**
     * Get public
     *
     * @return boolean
     */
    public function getPublic()
    {
        return $this->public;
    }

    /**
     * Set deleted
     *
     * @param boolean $deleted
     *
     * @return News
     */
    public function setDeleted($deleted)
    {
        $this->deleted = $deleted;

        return $this;
    }

    /**
     * Get deleted
     *
     * @return boolean
     */
    public function getDeleted()
    {
        return $this->deleted;
    }

    /**
     * Add newsArticle
     *
     * @param \Acme\Bundle\Entity\NewsArticle $newsArticle
     *
     * @return News
     */
    public function addNewsArticle(\Acme\Bundle\Entity\NewsArticle $newsArticle)
    {
        $this->newsArticle[] = $newsArticle;

        return $this;
    }

    /**
     * Remove newsArticle
     *
     * @param \Acme\Bundle\Entity\NewsArticle $newsArticle
     */
    public function removeNewsArticle(\Acme\Bundle\Entity\NewsArticle $newsArticle)
    {
        $this->newsArticle->removeElement($newsArticle);
    }

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

    /**
     * Add image
     *
     * @param \Acme\Bundle\Entity\NewsImage $image
     *
     * @return News
     */
    public function addImage(\Acme\Bundle\Entity\NewsImage $image)
    {
        $this->image[] = $image;

        return $this;
    }

    /**
     * Remove image
     *
     * @param \Acme\Bundle\Entity\NewsImage $image
     */
    public function removeImage(\Acme\Bundle\Entity\NewsImage $image)
    {
        $this->image->removeElement($image);
    }

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

    /**
     * Add medium
     *
     * @param \Acme\Bundle\Entity\Media $medium
     *
     * @return News
     */
    public function addMedia(\Acme\Bundle\Entity\Media $medium)
    {
        $this->media[] = $medium;

        return $this;
    }

    /**
     * Remove medium
     *
     * @param \Acme\Bundle\Entity\Media $medium
     */
    public function removeMedia(\Acme\Bundle\Entity\Media $medium)
    {
        $this->media->removeElement($medium);
    }

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

    /**
     * Add relatedFrom
     *
     * @param \Acme\Bundle\Entity\News $relatedFrom
     *
     * @return News
     */
    public function addRelatedFrom(\Acme\Bundle\Entity\News $relatedFrom)
    {
        $this->relatedFrom[] = $relatedFrom;

        return $this;
    }

    /**
     * Remove relatedFrom
     *
     * @param \Acme\Bundle\Entity\News $relatedFrom
     */
    public function removeRelatedFrom(\Acme\Bundle\Entity\News $relatedFrom)
    {
        $this->relatedFrom->removeElement($relatedFrom);
    }

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

    /**
     * Add relatedTo
     *
     * @param \Acme\Bundle\Entity\News $relatedTo
     *
     * @return News
     */
    public function addRelatedTo(\Acme\Bundle\Entity\News $relatedTo)
    {
        $this->relatedTo[] = $relatedTo;

        return $this;
    }

    /**
     * Remove relatedTo
     *
     * @param \Acme\Bundle\Entity\News $relatedTo
     */
    public function removeRelatedTo(\Acme\Bundle\Entity\News $relatedTo)
    {
        $this->relatedTo->removeElement($relatedTo);
    }

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

    /**
     * Set artist
     *
     * @param \Acme\Bundle\Entity\Artist $artist
     *
     * @return News
     */
    public function setArtist(\Acme\Bundle\Entity\Artist $artist = null)
    {
        $this->artist = $artist;

        return $this;
    }

    /**
     * Get artist
     *
     * @return \Acme\Bundle\Entity\Artist
     */
    public function getArtist()
    {
        return $this->artist;
    }
}

The NewsArticle entity:

<?php

// src/Acme/Bundle/Entity/NewsArticle.php

namespace Acme\Bundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\Table(name="news_article")
 */
class NewsArticle {

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

    /**
     * @ORM\ManyToOne(targetEntity="News", inversedBy="newsArticle")
     * @ORM\JoinColumn(name="news_id", referencedColumnName="id")
     */
    private $news;

    /**
     * @ORM\Column(type="string", length=150, unique=false, nullable=false)
     * @Assert\NotBlank()
     */
    private $headline;

    /**
     * @ORM\Column(type="string", length=150, nullable=true)
     */
    private $subheadline;

    /**
     * @ORM\Column(type="string", length=65536, nullable=false)
     * @Assert\NotBlank()
     */
    private $bodytext;

    /**
     * @ORM\ManyToOne(targetEntity="Language")
     * @ORM\JoinColumn(name="language_id", referencedColumnName="id")
     */
    private $languageId;


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

    /**
     * Set headline
     *
     * @param string $headline
     *
     * @return NewsArticle
     */
    public function setHeadline($headline)
    {
        $this->headline = $headline;

        return $this;
    }

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

    /**
     * Set subheadline
     *
     * @param string $subheadline
     *
     * @return NewsArticle
     */
    public function setSubheadline($subheadline)
    {
        $this->subheadline = $subheadline;

        return $this;
    }

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

    /**
     * Set bodytext
     *
     * @param string $bodytext
     *
     * @return NewsArticle
     */
    public function setBodytext($bodytext)
    {
        $this->bodytext = $bodytext;

        return $this;
    }

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

    /**
     * Set news
     *
     * @param \Acme\Bundle\Entity\News $news
     *
     * @return NewsArticle
     */
    public function setNews(\Acme\Bundle\Entity\News $news = null)
    {
        $this->news = $news;

        return $this;
    }

    /**
     * Get news
     *
     * @return \Acme\Bundle\Entity\News
     */
    public function getNews()
    {
        return $this->news;
    }

    /**
     * Set languageId
     *
     * @param \Acme\Bundle\Entity\Language $languageId
     *
     * @return NewsArticle
     */
    public function setLanguageId(\Acme\Bundle\Entity\Language $languageId = null)
    {
        $this->languageId = $languageId;

        return $this;
    }

    /**
     * Get languageId
     *
     * @return \Acme\Bundle\Entity\Language
     */
    public function getLanguageId()
    {
        return $this->languageId;
    }

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

The NewsType Class (except):

class NewsType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
// [...]
            ->add('relatedTo', EntityType::class, array(
                'class' => 'Bundle:News',
                'choice_label' => function($news) {
                    return $news->getNewsArticle();
                },
                'multiple' => true, 
                'expanded' => false,
                'required' => false
            ))
// [...]
        ;
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\Bundle\Entity\News',
        ));
    }
}

Any hints how to accomplish the goal? I would be grateful for any help!


EDIT #1:

Thanks to Terenoth I was able to pass the variable containing the default Language object to the NewsType class and followed his code example to filter the News entities having the default language. Solely returning the headline attribute (return $newsArticle->getHeadline();) fails with the following error message:

Attempted to call an undefined method named "getHeadline" of class "Doctrine\Common\Collections\ArrayCollection".

The new NewsType Class (except):

class NewsType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $searchedLanguage = $options['languageId'];
        $builder
            ->add('relatedTo', EntityType::class, array(
                'class' => 'Bundle:News',
                'query_builder' => function (EntityRepository $er) {
                    return $er->createQueryBuilder('a')
                        ->where('a.deleted = 0')
                        ->orderBy('a.validFrom', 'ASC');
                },
                'choice_label' => function($relatedTo, $searchedLanguage) {
                    $newsArticle = $relatedTo
                            ->getNewsArticle()
                            ->filter(
                                function(NewsArticle $newsArticle) use ($searchedLanguage) {
                                    return $newsArticle->getLanguageId() === $searchedLanguage;
                                }
                            )
                    ;
                    return $newsArticle->getHeadline();
                },
                'multiple' => true, 
                'expanded' => false,
                'required' => false
            ))
        ;
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\Bundle\Entity\News',
            'languageId' => 'Acme\Bundle\Entity\Language',
        ));
    }
}

What can I do to get the NewsArticle entity instead of the ArrayCollection?

Thank´s in advance!

Upvotes: 2

Views: 5123

Answers (1)

Terenoth
Terenoth

Reputation: 2598

The way you defined your mapping, $newsArticles in News is a OneToMany relation, meaning there are several NewsArticles for each News. So obviously, getNewsArticles returns a Collection of NewsArticles.

Either you defined your mapping wrong, and there should be only 1 NewsArticle by News, so replace your OneToMany by a OneToOne... Either you should select on of the several NewsArticle to display its title.

EDIT: Following your indications in the comments, here's what you should do (assuming you have $searchedLanguage in a variable).

'choice_label' => function($news) {
    $article = $news->getNewsArticle()->filter(function (NewsArticle $article) use ($searchedLanguage) {
        return $this->getLanguageId() === $searchedLanguage;
    });

    return $article->getHeadline();
},

Upvotes: 1

Related Questions