Форма Symfony: OneToMany и поле CollectionType

Я застрял в создании формы с Symfony 3.

Я определил объект «Новости», содержащий атрибут «newsArticle», который имеет отношение OneToMany к объекту «NewsArticle», содержащий переводы его атрибутов «заголовок», «подзаголовок» и «основной текст». Цель состоит в том, чтобы предоставить форму, которая содержит, с одной стороны, поля для атрибутов «Новостей», а с другой стороны, поля для создания записи «НовостиСтатьи» для языка по умолчанию.

Новости организации (отрывок):

/**
 * @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"})
     */
    private $newsArticle;

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

Entity NewsArticle (отрывок):

/**
 * @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")
     * @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;
}

Контролер (отрывок):

public function addAction(Request $request) {
    $lang = $this->getDoctrine()
        ->getRepository('Bundle:Language')
        ->findOneBy(array('deleted' => 0, 'fallback' => 1));

    $news = new News();
    //$news->newsArticle->setLanguageId($lang);

    $form = $this->createForm(NewsType::class, $news);

    $form->handleRequest($request);

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

        $em = $this->getDoctrine()->getManager();
        $em->persist($news);
        $em->flush();
    }

    return $this->render('Bundle:News:add.html.twig', array(
        'form' => $form->createView(),
        'news' => $news,
    ));
}

Шаблон TWIG (отрывок):

{{ form_start(form) }}
    <div class="form-group">
        {{ form_label(form.headline) }}
        <div class="col-sm-8 col-md-6">
            {{ form_errors(form.headline) }}
            {{ form_widget(form.headline) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(form.subheadline) }}
        <div class="col-sm-8 col-md-6">
            {{ form_errors(form.subheadline) }}
            {{ form_widget(form.subheadline) }}
        </div>
    </div>
    <div class="form-group">
        {{ form_label(form.bodytext) }}
        <div class="col-sm-8 col-md-6">
            {{ form_errors(form.bodytext) }}
            {{ form_widget(form.bodytext) }}
        </div>
    </div>
{{ form_end(form) }}

NewsType (отрывок):

class NewsType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
            ->add('newsArticle', CollectionType::class, array(
                'entry_type'   => NewsArticleType::class,
                'allow_add'   => true,
                'allow_delete'   => true,
            ))
    }

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

NewsArticleType (отрывок):

class NewsArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
            ->add('headline', TextType::class, array(
                'required' => false,
            ))
            ->add('subheadline', TextType::class, array(
                'required' => false,
            ))
            ->add('bodytext', CKEditorType::class, array(
                'required' => false,
            ))
        ;
    }

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

К сожалению, я получаю сообщение об ошибке...

Ни свойство "headline", ни один из методов "headline()", "getheadline()"/"isheadline()" или "__call()" не существуют и не имеют общего доступа в классе "Symfony\Component\Form\FormView" в Bundle:_Partials:Form/news.html.twig в строке 27

... и понятия не имею, как получить доступ к атрибутам связанного объекта? Любые подсказки?

РЕДАКТИРОВАНИЕ №1:

Полный «дочерний» объект, включая геттеры/сеттеры:

/**
 * @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 \Bundle\Entity\News $news
     *
     * @return NewsArticle
     */
    public function setNews(\Bundle\Entity\News $news = null)
    {
        $this->news = $news;

        return $this;
    }

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

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

        return $this;
    }

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

Существуют геттеры и сеттеры для атрибутов, но мне интересно, почему в сообщении об ошибке отсутствует метод getheadline() вместо getHзаголовок()? Может быть проблема в неправильном синтаксисе в TWIG: {{ form_errors(form.headline) }} ? Я также пробовал {{ form_errors(form.newsArticle.headline) }}, но сообщение об ошибке остается прежним.

Любые подсказки, пожалуйста?


person Pixelrocker    schedule 10.06.2016    source источник
comment
Почитайте немного об атрибутах private/protected и public: php.net/manual/ ru/language.oop5.visibility.php. Как только вы это поймете, потребность в геттерах/сеттерах станет очевидной. Возможно, вам также захочется взглянуть на документы Symfony. Множество примеров использования сущностей и форм.   -  person Cerad    schedule 11.06.2016
comment
Даже если я сделаю все атрибуты дочерней сущности NewsArticle общедоступными, я получу то же сообщение об ошибке. Я читал Symfony Docs, особенно главу, посвященную типам форм, но, к сожалению, там нет примеров для такого рода вложенных форм. Упоминается только, что CollectionType выполнит эту работу: в более сложных примерах вы можете встраивать целые формы, что полезно при создании форм, которые раскрывают отношения «один ко многим» (symfony.com/doc/current/reference/forms/types/collection.html)   -  person Pixelrocker    schedule 11.06.2016
comment
Вы читали кулинарную книгу? symfony.com/doc/current/cookbook/form/form_collections.html   -  person Cerad    schedule 13.06.2016
comment
Да, я сделал, но в данном случае это бесполезно. Я пока не нашел похожего примера.   -  person Pixelrocker    schedule 15.06.2016


Ответы (2)


Думаю проблема как раз в сеттерах-геттерах

Когда вы попробуете эту команду:

php bin/console doctrine:generate:entities youBundle

для создания геттера/сеттера или записи его

И вы забыли inversedBy в своей сущности NewsActicle

как это :

    /**
     * @ORM\ManyToOne(targetEntity="News", inversedBy="newsArticle" )
     * @ORM\JoinColumn(name="news_id", referencedColumnName="id")
     */
    private $news;
person Shigiang Liu    schedule 11.06.2016
comment
Верно, я использую консольную команду doctrine:generate:entities для генерации геттеров и сеттеров. Спасибо за подсказку добавить 'inversedBy' - к сожалению, сообщение об ошибке остается :( Я опубликую геттеры/сеттеры в надежде, что ваша догадка укажет правильное направление. - person Pixelrocker; 11.06.2016

В конце концов, я добился некоторого прогресса и избавился от сообщения об ошибке.

Одна проблема в моем TWIG заключалась в том, что $newsArticle — это массив объектов, а не объект, поэтому мне нужно сначала перебрать массив:

{% for newsArticle in form.newsArticle %}
<div class="form-group">
    {{ form_label(newsArticle.headline) }}
    <div class="col-sm-8 col-md-6">
        {{ form_errors(newsArticle.headline) }}
        {{ form_widget(newsArticle.headline) }}
    </div>
</div>
<div class="form-group">
    {{ form_label(newsArticle.subheadline) }}
    <div class="col-sm-8 col-md-6">
        {{ form_errors(newsArticle.subheadline) }}
        {{ form_widget(newsArticle.subheadline) }}
    </div>
</div>
<div class="form-group">
    {{ form_label(newsArticle.bodytext) }}
    <div class="col-sm-8 col-md-6">
        {{ form_errors(newsArticle.bodytext) }}
        {{ form_widget(newsArticle.bodytext) }}
    </div>
</div>
{% endfor %}

Еще одним важным изменением стало создание экземпляра объекта NewsArticle сначала в NewsController:

public function addAction(Request $request) {
    $lang = $this->getDoctrine()
        ->getRepository('Bundle:Language')
        ->findOneBy(array('deleted' => 0, 'fallback' => 1));

    $article = new NewsArticle();
    $article->setLanguageId($lang);

    $news = new News();
    $news->setPublic(0);
    $news->setDeleted(0);
    $news->setIdent($hash);
    $news->getNewsArticle()->add($article);


    $form = $this->createForm(NewsType::class, $news);

    $form->handleRequest($request);

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

        $em = $this->getDoctrine()->getManager();
        $em->persist($news);
        $em->flush();

        $this->addFlash(
            'success',
            'Your changes were saved!'
        );
    }

    return $this->render('Bundle:News:add.html.twig', array(
        'form' => $form->createView(),
        'news' => $news,
    ));
}

Я не думаю, что это лучшее решение (в частности, повторение массива, который имеет только один объект), но, по крайней мере, я не получаю сообщения об ошибке, и на данный момент это работает. Если у вас есть идеи по улучшению, пожалуйста, дайте мне знать - я был бы очень признателен за вашу помощь!

person Pixelrocker    schedule 17.06.2016