There are three entities related by ManyToOne relations: A 'Page' can have many 'Block'. (A block can be related to one page). A 'Block' can have many 'Button', (A 'Button' can be in one block).
I have a form system where I can add multiple Blocks, and within each block multiple Buttons. So there is an embedded form inside an embedded for ( 2 layers)
The problem is that only one button gets saved to the database when trying to save multiple.
But when i save one button, I can go back and save multiple at once. I am not able to understand this behavior.
/**
* #ORM\Entity(repositoryClass="App\Repository\PageRepository")
*/
class Page
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
...
/**
* #ORM\OneToMany(targetEntity="App\Entity\Block", mappedBy="page", orphanRemoval=true, cascade={"persist"})
* #ORM\OrderBy({"ordering" = "ASC"})
*/
private $blocks;
...
public function __construct()
{
$this->blocks = new ArrayCollection();
}
/**
* #return Collection|Block[]
*/
public function getBlocks(): Collection
{
return $this->blocks;
}
public function addBlock(Block $block): self
{
if (!$this->blocks->contains($block)) {
$this->blocks[] = $block;
$block->setPage($this);
}
return $this;
}
public function removeBlock(Block $block): self
{
if ($this->blocks->contains($block)) {
$this->blocks->removeElement($block);
// set the owning side to null (unless already changed)
if ($block->getPage() === $this) {
$block->setPage(null);
}
}
return $this;
}
}
Block:
/**
* #ORM\Entity(repositoryClass="App\Repository\BlockRepository")
*/
class Block
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Block\Button", mappedBy="block",
orphanRemoval=true, cascade={"persist"})
*/
private $buttons;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Page", inversedBy="blocks")
* #ORM\JoinColumn(nullable=false)
*/
private $page;
public function __construct()
{
$this->buttons = new ArrayCollection();
}
/**
* #return Collection|Button[]
*/
public function getButtons(): Collection
{
return $this->buttons;
}
public function addButton(Button $button): self
{
if (!$this->buttons->contains($button)) {
$this->buttons[] = $button;
$button->setBlock($this);
}
return $this;
}
public function removeButton(Button $button): self
{
if ($this->buttons->contains($button)) {
$this->buttons->removeElement($button);
// set the owning side to null (unless already changed)
if ($button->getBlock() === $this) {
$button->setBlock(null);
}
}
return $this;
}
}
Button:
/**
* #ORM\Entity(repositoryClass="App\Repository\Block\ButtonRepository")
* #ORM\Table(name="block_button")
*/
class Button
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Block", inversedBy="buttons")
* #ORM\JoinColumn(nullable=false)
*/
private $block;
/**
* #ORM\Column(type="string", length=255)
*/
private $label;
public function getBlock(): ?Block
{
return $this->block;
}
public function setBlock(?Block $block): self
{
$this->block = $block;
return $this;
}
}
Following are the form corresponding form types:
PageType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('blocks', CollectionType::class, [
'entry_type' => BlockType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'help' => '<a data-collection="add" class="btn btn-info btn-sm" href="#">Add Block</a>',
'help_html' => true
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Page::class,
]);
}
}
BlockType
class BlockType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('buttons', CollectionType::class, [
'entry_type' => BlockButtonType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'help' => '<a data-collection="add" class="btn btn-info btn-sm" href="#">Add Button</a>',
'help_html' => true,
'attr' => [
'data-field' => 'buttons'
]
])
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Block::class,
]);
}
}
BlockButtonType
class BlockButtonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Button::class,
]);
}
}
The responsible methods to handle form new and edit are as follows:
/**
* #Route("/admin/pages")
*/
class PageController extends AbstractController
{
/**
* #Route("/new", name="admin_page_new", methods={"GET","POST"})
*/
public function new(Request $request, FileUploader $fileUploader):Response
{
$page = new Page();
$form = $this->createForm(PageType::class, $page);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($form->get('publish')->isClicked()) {
$page->setPublished(true);
}
// handle uploads here
$this->handleUploads($fileUploader, $page, $form);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($page);
$entityManager->flush();
if ($form->get('close')->isClicked()) {
return $this->redirectToRoute('admin_page_index');
}
$this->addFlash('success', 'Page saved');
return $this->redirectToRoute('admin_page_edit', [
'id' => $page->getId(),
]);
}
return $this->render('admin/page/new.html.twig', [
'page' => $page,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}/edit", name="admin_page_edit", methods={"GET","POST"})
*/
public function edit(Request $request, FileUploader $fileUploader, Page $page): Response
{
$form = $this->createForm(PageType::class, $page);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($page->getParent() != null && $page->getParent()->getId() == $page->getId()) {
$page->setParent(null);
$this->addFlash('error', 'You cannot set a page to be a parent of itself');
return $this->render('admin/page/edit.html.twig', [
'page' => $page,
'form' => $form->createView(),
]);
}
if ($form->get('publish')->isClicked()) {
$page->setPublished(true);
}
// get all blocks and check if anything was uploaded?
$this->handleUploads($fileUploader, $page, $form);
$this->getDoctrine()->getManager()->flush();
if ($form->get('close')->isClicked()) {
return $this->redirectToRoute('admin_page_index');
}
$this->addFlash('success', 'Page saved');
return $this->redirectToRoute('admin_page_edit', [
'id' => $page->getId(),
]);
}
}
Finally the html twig that displays the form:
{% extends 'admin/layouts/admin.html.twig' %}
{% block body %}
{{ form_start(form) }}
<div class="tab-content">
<div class="tab-pane show active" id="details">
{{ form_row(form.blocks) }}
</div>
</div>
{{ form_widget(form.save) }}
{{ form_widget(form.close) }}
{{ form_widget(form.publish) }}
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
Related
I'm trying to create a form for an article with an addon (tender).
The article is in one-to-one relationship with the addon and the addon can be disabled (null).
If the checkbox with an addon is checked, the addon needs to be validated, then submitted together with the article.
The issue I'm having is, I'm unable to set the addon to null before the submission, what produces the errors - the form still tries to submit an addon tied to the article, while its values can't be empty.
How can I disable the addon when the checkbox is unchecked and still submit the article to the database, but without the addon?
I've tried using form events to modify the event's data, but it turned out that tender is not part of data during PRE_SUBMIT.
If I try to modify the data during SUBMIT, then I have to alter the logic in the methods of an entity, but it doesn't feel right and also causes some issues that I don't remember and couldn't work around.
I've also tried using 'empty_data' => null, thinking it would do exactly what I've wanted, but it turned out it has no effect on my form at all.
Article's entity:
<?php
namespace App\Entity;
use App\Repository\ContentsRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ContentsRepository::class)
*/
class Contents
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity=ContentsTenders::class, mappedBy="content", cascade={"persist", "remove"})
*/
private $tender;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): ?int
{
return $this->position;
}
public function getTender(): ?ContentsTenders
{
return $this->tender;
}
public function setTender(ContentsTenders $tender): self
{
$this->tender = $tender;
// set the owning side of the relation if necessary
if ($tender->getContent() !== $this) {
$tender->setContent($this);
}
return $this;
}
}
Addon's entity:
<?php
namespace App\Entity;
use App\Repository\ContentsTendersRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ContentsTendersRepository::class)
*/
class ContentsTenders
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity=Contents::class, inversedBy="tender", cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $content;
public function getId(): ?int
{
return $this->id;
}
public function getContent(): ?Contents
{
return $this->content;
}
public function setContent(Contents $content): self
{
$this->content = $content;
return $this;
}
}
Article's form:
class ContentsType extends AbstractType
{
//public function __construct(EntityManagerInterface $em, UserInterface $user)
//{
// $this->em = $em;
// $this->user = $user;
//}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('isTender', CheckboxType::class, [
'mapped' => false,
'required' => false,
'label' => 'Przetarg',
])
->add('tender', NewTendersType::class, [
'required' => false,
'constraints' => array(new Valid()),
'empty_data' => null
])
->add('save', SubmitType::class, [
'label' => 'Zapisz',
'attr' => ['class' => 'save'],
])
;
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if(!$form['isTender']->getData()){
//
}
}
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contents::Class,
'accessible_ids' => [],
]);
}
}
#Edit
I've used:
Symfony Custom Field or Dynamic Form?
and added:
class NullableTenderTransformer implements DataTransformerInterface
{
public function transform($value)
{
return $value;
}
public function reverseTransform($data)
{
if($data->getTender()->getTenContracting() === null
&& $data->getTender()->getTenOrderFor() === null
&& $data->getTender()->getTenNumber() === null) {
$data->resetTender();
}
return $data;
}
}
combined with the answer below and $content->setTender(new ContentsTenders()); in the controller, +reset method in model, it works.
It might not be a perfect solution, though.
You can use a validation group
'constraints' => array(new Valid(['groups' => "withTender")
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Contents::Class,
'accessible_ids' => [],
'validation_groups' => function (FormInterface $form): array {
$data = $form->getData();
$groups = ['Default'];
if ($data->isTender) {
$groups[] = 'withTender';
}
return $groups;
}
]);
}
I have this error when trying to create new "Category", it works befare, but not now.
Error gives me after modify "Recipes" form for add other relation ManyToMany
Blockquote Neither the property "category" nor one of the methods "category()", "getcategory()"/"iscategory()"/"hascategory()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
Category.php
<?php
namespace App\Entity;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\CategoryRepository", repositoryClass=CategoryRepository::class)
*/
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $title;
/**
* #ORM\Column(type="string", length=1000, nullable=true)
*/
private $description;
/**
* Bidirectional (INVERSE SIDE)
*
* #ORM\ManyToMany(targetEntity=Recipe::class, mappedBy="category")
*/
private $recipes;
public function __construct()
{
$this->recipes = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
/**
* #return Collection|Recipe[]
*/
public function getRecipes(): Collection
{
return $this->recipes;
}
public function addRecipe(Recipe $recipe): self
{
if (!$this->recipes->contains($recipe)) {
$this->recipes[] = $recipe;
$recipe->addCategory($this);
}
return $this;
}
public function removeRecipe(Recipe $recipe): self
{
if ($this->recipes->removeElement($recipe)) {
$recipe->removeCategory($this);
}
return $this;
}
public function __toString()
{
return $this->title;
}
}
Recipe.php
<?php
namespace App\Entity;
use App\Repository\RecipeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RecipeRepository::class)
*/
class Recipe {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $title;
/**
* #ORM\Column(type="string", length=1000)
*/
private $description;
/**
* #ORM\Column(type="datetime")
*/
private $date;
/**
* Bidirectional - Many recipes have Many categories (OWNING SIDE)
*
* #ORM\ManyToMany(targetEntity=Category::class, inversedBy="recipes")
*/
private $category;
/**
* #ORM\OneToMany(targetEntity=Step::class, mappedBy="recipe", cascade={"persist"})
*/
private $steps;
public function __construct() {
$this->category = new ArrayCollection();
$this->steps = new ArrayCollection();
}
public function getId(): ?int {
return $this->id;
}
public function getTitle(): ?string {
return $this->title;
}
public function setTitle(string $title): self {
$this->title = $title;
return $this;
}
public function getDate(): ?\DateTimeInterface {
return $this->date;
}
public function setDate(\DateTimeInterface $date): self {
$this->date = $date;
return $this;
}
public function getDescription(): ?string {
return $this->description;
}
public function setDescription(string $description): self {
$this->description = $description;
return $this;
}
/**
* #return Collection|Category[]
*/
public function getCategory(): Collection {
return $this->category;
}
public function addCategory(Category $category): self {
if (!$this->category->contains($category)) {
$this->category[] = $category;
}
return $this;
}
public function removeCategory(Category $category): self {
$this->category->removeElement($category);
return $this;
}
public function hasCategory() {
if($this->category->isEmpty()){
return true;
}
return false;
}
/**
* #return Collection|Step[]
*/
public function getSteps(): Collection {
return $this->steps;
}
public function addStep(Step $step): self {
if (!$this->steps->contains($step)) {
$this->steps[] = $step;
$step->setRecipe($this);
}
return $this;
}
public function removeStep(Step $step): self {
if ($this->steps->removeElement($step)) {
// set the owning side to null (unless already changed)
if ($step->getRecipe() === $this) {
$step->setRecipe(null);
}
}
return $this;
}
public function __toString() {
return $this->title;
}
}
new.html.twig (for new category)
{% extends 'base.html.twig' %}
{% block title %}New Category{% endblock %}
{% block body %}
<h1>New Category</h1>
{{ form(form) }}
{% endblock %}
new.html.twig (for new recipe)
{% extends 'base.html.twig' %}
{% block title %}New Recipe{% endblock %}
{% block body %}
<h1>New Recipe</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
{{ form_widget(form.category) }}
<ul class="steps list-unstyled" data-prototype="{{ form_widget(form.steps.vars.prototype)|e }}">
{{ form_end(form) }}
{% endblock %}
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script>
var $collectionHolder;
// setup an "add a tag" link
var $saveButton = $('#recipe_save');
var $addStepButton = $('<button type="button" class="add_step_link btn btn-secondary btn-sm mb-2 mt-2">Add Step</button>');
var $newLinkLi = $('<li></li>');
jQuery(document).ready(function () {
// Get the ul that holds the collection of tags
$collectionHolder = $('ul.steps');
$collectionHolder.before($addStepButton);
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find('input').length);
$addStepButton.on('click', function (e) {
// add a new tag form (see next code block)
addStepForm($collectionHolder, $newLinkLi);
jQuery('form').append($saveButton);
});
});
function addStepForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
var newForm = prototype;
// You need this only if you didn't set 'label' => false in your tags field in TaskType
// Replace '__name__label__' in the prototype's HTML to
// instead be a number based on how many items we have
// newForm = newForm.replace(/__name__label__/g, index);
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
newForm = newForm.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<div></div>').append(newForm);
// also add a remove button, just for this example
$newFormLi.prepend('X');
$newLinkLi.before($newFormLi);
// handle the removal, just for this example
$('.remove-tag').click(function (e) {
e.preventDefault();
$(this).parent().remove();
return false;
});
}
</script>
{% endblock %}
CategoryType.php
<?php
namespace App\Form;
use App\Entity\Category;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class CategoryType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('title', TextType::class, [
'label' => 'Category Title:',
'attr' => [
'class' => 'form-control',
'placeholder' => 'Category Title'
]
])
->add('description', TextareaType::class, [
'label' => 'Category Description:',
'attr' => [
'class' => 'form-control',
'placeholder' => 'Category Description'
]
])
->add('save', SubmitType::class, [
'attr' => [
'class' => 'btn btn-primary btn-block'
]
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
}
RecipeType.php
<?php
namespace App\Form;
use App\Entity\Recipe;
use App\Entity\Category;
use App\Form\StepType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class RecipeType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('title', TextType::class, [
'attr' => [
'class' => 'form-control',
'placeholder' => 'Recipe Title'
]
])
->add('description', TextareaType::class, [
'attr' => [
'class' => 'form-control',
'placeholder' => 'Recipe Description'
]
])
->add('category', EntityType::class, [
'class' => Category::class,
'multiple' => true,
'mapped' => true,
'required' => false,
'attr' => [
'class' => 'form-control',
]
])
->add('steps', CollectionType::class, [
'label' => false,
'entry_type' => StepType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'by_reference' => false,
])
->add('save', SubmitType::class, [
'attr' => [
'id' => 'save',
'class' => 'btn btn-success btn-block'
]
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Recipe::class,
]);
}
}
The runtime error references {{ form_widget(form.category) }} from new.html.twig (for Recipes).
My suspicion is that the error is somewhere combined with the Controller or maybe with the Router. Are you sure in the Controller action to create a new category, you are rendering the category page (something like
return $this->render('category/new.html.twig');
and not mistakenly the recipes page? So it's trying to find form.category but the category doesn't have a category itself.
I've been staring at this too long, and now I need another set of eyes.
I had previously set up my relationship between two entities, Dish and Ingredient as a many to many relationship, and had the form association working perfectly. However, I realized afterward that I needed another field on my link table to specify "amount" so as a result I created a new entity DishIngredient. It created the same table but instead of a many to many, it's now one table with two Many to One relationships to Dish and Ingredient.
So far so good. I have everything working until I attempt to associated ingredients with dishes. I'm able to successfully generate a form to display each of the Ingredients, however when I submit it, I run into problems. It's attempting to submit to Ingredient instead of DishIngredient.
Here is what I have so far:
My controller:
/**
* #Route("/recipe/ingredient/{id}", name="edit_ingredients")
*/
public function ingredientEdit(Request $request, Dish $dish, $id)
{
$user = $this->getUser();
$dish_id = $dish->getId();
$dish = $this->getDoctrine()->getRepository(Dish::class)->findOneBy(array('id' =>$dish_id ));
$dish_user = $dish->getUser();
//in case someone tries to manually hack into someone else's recipe
if($dish_user != $user) {
return $this->render('dishes/error.html.twig', array(
'dish' => $dish,
'user' => $user
)
);
}
else {
$form = $this->createForm(IngredientType::class, $dish);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$dish = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($dish);
$em->flush();
return $this->redirectToRoute('edit_ingredients', array('id' => $id));
}
return $this->render('dishes/ingredients.html.twig', array(
'form' => $form->createView(),
'dish' => $dish
)
);
}
}
Which then loads this form:
class IngredientType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options); // TODO: Change the autogenerated stub
$ingredient = new Ingredient();
$builder
->add('DishIngredients',EntityType::class, array(
'required' => false,
'attr' => array('class' => 'form-control'),
'class' => Ingredient::class,
'query_builder' => function(IngredientRepository $ir) {
return $ir->createQueryBuilder('s')
->orderBy('s.name', 'ASC');
},
'multiple' => true,
'expanded' => true,
))
->add('save', SubmitType::class, array(
'label' => 'Update',
'attr' => array('class' => 'btn btn-primary mt-3')
))
->getForm();
}
}
It displays fine in the twig with the following:
{{ form_start(form) }}
{% for i in form.DishIngredients %}
{{ form_widget(i) }} {{ form_label(i) }}<br>
{% endfor %}
{{ form_end(form) }}
However the problem is that when I attempt to submit it, I get this error:
Expected value of type "App\Entity\DishIngredient" for association
field "App\Entity\Dish#$dishIngredients", got "App\Entity\Ingredient"
instead.
If I change the form to call DishIngredient instead of Ingredient, it shows none of the ingredient; it just creates a text field looking for an input; not what I want or need at all.
Here's the DishIngredient Entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\DishIngredientRepository")
*/
class DishIngredient
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Dish", inversedBy="dishIngredients")
*/
private $dish;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Ingredient", inversedBy="dishIngredients")
*/
private $ingredient;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $amount;
public function getId(): ?int
{
return $this->id;
}
public function getDish(): ?Dish
{
return $this->dish;
}
public function setDish(?Dish $dish): self
{
$this->dish = $dish;
return $this;
}
public function getIngredient(): ?Ingredient
{
return $this->ingredient;
}
public function setIngredient(?Ingredient $ingredient): self
{
$this->ingredient = $ingredient;
return $this;
}
public function getAmount(): ?string
{
return $this->amount;
}
public function setAmount(?string $amount): self
{
$this->amount = $amount;
return $this;
}
}
and here's the Ingredient entity for reference
/**
* #ORM\Entity(repositoryClass="App\Repository\IngredientRepository")
*/
class Ingredient
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\DishIngredient", mappedBy="ingredient")
*/
private $dishIngredients;
public function __toString(): ?string
{
// TODO: Implement __toString() method.
return $this->name;
}
public function __construct()
{
$this->dishes = new ArrayCollection();
$this->dishIngredients = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* #return Collection|DishIngredient[]
*/
public function getDishIngredients(): Collection
{
return $this->dishIngredients;
}
public function addDishIngredient(DishIngredient $dishIngredient): self
{
if (!$this->dishIngredients->contains($dishIngredient)) {
$this->dishIngredients[] = $dishIngredient;
$dishIngredient->setIngredient($this);
}
return $this;
}
public function removeDishIngredient(DishIngredient $dishIngredient): self
{
if ($this->dishIngredients->contains($dishIngredient)) {
$this->dishIngredients->removeElement($dishIngredient);
// set the owning side to null (unless already changed)
if ($dishIngredient->getIngredient() === $this) {
$dishIngredient->setIngredient(null);
}
}
return $this;
}}
Anyone have any ideas here?
--
EDIT:
Okay, so I created a new form titled DishIngredientType, and moved the information from above into it, and then modified IngredientType to have following instead:
$builder
->add('DishIngredients',CollectionType::class, array(
'required' => false,
'attr' => array('class' => 'form-control'),
'entry_type' => DishIngredientType::class,
'entry_options' => ['label' => false],
))
but now I'm only getting a plan text field (no options at all).
This is how they look in DishIngredientType
->add('DishIngredients',EntityType::class, array(
'required' => false,
'attr' => array('class' => 'form-control'),
'class' => Ingredient::class,
'query_builder' => function(IngredientRepository $ir) {
return $ir->createQueryBuilder('s')
->orderBy('s.name', 'ASC');
},
'multiple' => true,
'expanded' => true,
))
I'm not sure what I'm missing here. I know this shouldn't be this difficult.
I have a many to many relation that is actually made of two many to ones on a third entity.
Domain ← one-to-many → DomainTag ← many-to-one → Tag
I'm building a Rest API, and I want to add domains to a Tag via a specific route. Everything works fine when the tags has no association.
But when I try to add new domains to a tag, there is an error if the tag already as one (or more) domain(s).
The error is
The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class AppBundle\Entity\Domain. You can avoid this error by setting the "data_class" option to "AppBundle\Entity\Domain" or by adding a view transformer that transforms an instance of class AppBundle\Entity\Domain to scalar, array or an instance of \ArrayAccess.
Here are the three entities :
Tag
class Tag
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="tag", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
DomainTag
class DomainTag
{
…
/**
* #var Tag
*
* #ORM\ManyToOne(targetEntity="Tag", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="tag_id", referencedColumnName="id")
* })
*/
private $tag;
/**
* #var Domain
*
* #ORM\ManyToOne(targetEntity="Domain", inversedBy="domainTags")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="domain_id", referencedColumnName="id")
* })
*/
private $domain;
…
}
Domain
class Domain
{
…
/**
* #ORM\OneToMany(targetEntity="DomainTag", mappedBy="domain", cascade={"persist", "remove"})
*/
private $domainTags;
…
}
Here is the form
class AddDomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('domainTags', 'collection', array(
'type' => new DomainTagFormType(),
'allow_add' => true,
))
;
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tag',
'csrf_protection' => false,
));
}
public function getName()
{
return 'api_set_domaintag';
}
}
DomainTagFormType
class DomainTagFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('weight')
->add('domain', 'entity', array(
'class' => 'AppBundle:Domain',
'property' => 'id',
'multiple' => false,
'expanded' => true
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\DomainTag',
'csrf_protection' => false,
));
}
// BC for SF < 2.7
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
public function getName()
{
return 'api_domaintag';
}
}
And finally, the controller
/**
* #Rest\Put("/{id}/domains", requirements={"id" = "\d+"})
* #Security("has_role('ROLE_SUPER_ADMIN')")
* #Rest\View
*/
public function setDomainAction(Request $request, $id)
{
$tagManager = $this->get('app.manager.tag');
$domainManager = $this->get('app.manager.domain');
$tag = $tagManager->find($id);
if (!$tag) {
return new Response('Tag not found', 404);
}
$form = $this->createForm(new AddDomainTagFormType(), $tag, array('method' => 'PUT'));
$form->handleRequest($request);
if ($form->isValid()) {
foreach ($tag->getDomainTags() as $dt) {
$dt->setTag($tag);
$tagManager->persist($dt);
}
$tagManager->flush($tag);
return new Response('', 204);
} else {
return $this->view($form, 400);
}
}
Do I have to remove all DomainTags from the Tag and the recreate all associations ? Or is there a way to just add/remove DomainTags ?
Thank you
I'm working on a form that handle N:M relationship with an extra parameter (extra field/column). This is what I've done until now:
In OrdersType.php form:
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder fields
// $builder fields only need on edit form not in create
if ($options['curr_action'] !== NULL)
{
$builder
// other $builder fields
->add("orderProducts", "collection", array(
'type' => new OrdersHasProductType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE,
'intention' => 'orders_form',
'curr_action' => NULL
));
}
public function getName()
{
return 'orders';
}
}
In OrderHasProductType.php:
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
->add('amount', 'text', array(
'required' => TRUE,
'label' => FALSE
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\OrderHasProduct',
'intention' => 'order_has_product'
));
}
public function getName()
{
return 'order_has_product';
}
}
And finally this is Orders.php and OrdersHasProduct.php entities:
class Orders {
use IdentifiedAutogeneratedEntityTrait;
// rest of fields for the entity
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order", cascade={"all"})
*/
protected $orderProducts;
protected $products;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function __construct()
{
$this->orderProducts = new ArrayCollection();
$this->products = new ArrayCollection();
}
public function getOrderProducts()
{
return $this->orderProducts;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
public function getProduct()
{
$products = new ArrayCollection();
foreach ($this->orderProducts as $op)
{
$products[] = $op->getProduct();
}
return $products;
}
public function setProduct($products)
{
foreach ($products as $p)
{
$ohp = new OrderHasProduct();
$ohp->setOrder($this);
$ohp->setProduct($p);
$this->addPo($ohp);
}
}
public function getOrder()
{
return $this;
}
public function addPo($ProductOrder)
{
$this->orderProducts[] = $ProductOrder;
}
public function removePo($ProductOrder)
{
return $this->orderProducts->removeElement($ProductOrder);
}
}
/**
* #ORM\Entity
* #ORM\Table(name="order_has_product")
* #Gedmo\SoftDeleteable(fieldName="deletedAt")
* #UniqueEntity(fields={"order", "product"})
*/
class OrderHasProduct {
use IdentifiedAutogeneratedEntityTrait;
/**
* Hook timestampable behavior
* updates createdAt, updatedAt fields
*/
use TimestampableEntity;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders", inversedBy="orderProducts")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product", inversedBy="orderProducts")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
/**
* #ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
protected $deletedAt;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
}
But when I try to edit a order with this code in my controller:
public function editAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$params = explode('::', $request->attributes->get('_controller'));
$actionName = substr($params[1], 0, -6);
$orderForm = $this->createForm(new OrdersType(), $order, array('action' => '#', 'method' => 'POST', 'register_type' => $type, 'curr_action' => $actionName));
return array(
"form" => $orderForm->createView(),
'id' => $id,
'entity' => $order
);
}
I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product. You can avoid this
error by setting the "data_class" option to
"Proxies__CG__\Tanane\ProductBundle\Entity\Product" or by adding a
view transformer that transforms an instance of class
Proxies__CG__\Tanane\ProductBundle\Entity\Product to scalar, array or
an instance of \ArrayAccess.
And I don't find or know how to fix it, can any give me some help?
I think this is happening because this code
class OrdersHasProductType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', 'text', array(
'required' => FALSE,
'label' => FALSE
))
));
}
//...
}
means that Symfony is expecting "product" to be a field of type "text", but when it calls getProduct() on the OrderHasProduct it gets a Product object (or a Doctrine Proxy to a Product, since it's not been loaded at that point). Symfony Fields inherit from Form/AbstractType, so they're essentially Forms in their own right, with just one field, hence the error message.
The solution is either to make that field of type "entity", or to create a different method which only gives the name of the Product, e.g. getProductName() on OrderHasProduct, and then use that as the data behind the field.