ManytoMany ArrrayCollection In forms Symfony - php

I'm working on a Symfony project where an I have two types of User Client and EmployeSpie, both have their own entity.
When you create/edit a user you can link EmployeSpie to a CLient.
That's where is my problem, When I edit or create a user I can create a user but nothing is store inside my table which make the link between my table Client and EmployeSpie.
Here is what I've done:
my entity Client having this:
class Client extends User
{
/**
* #ORM\ManyToMany(targetEntity=EmployeSpie::class, mappedBy="clients", cascade={"persist"})
*/
private $employeSpies;
/**
* #return Collection|EmployeSpie[]
*/
public function getEmployeSpies(): Collection
{
return $this->employeSpies;
}
public function addEmployeSpy(EmployeSpie $employeSpy): self
{
if (!$this->employeSpies->contains($employeSpy)) {
$this->employeSpies[] = $employeSpy;
$employeSpy->addClientEmploye($this);
}
return $this;
}
public function removeEmployeSpy(EmployeSpie $employeSpy): self
{
if ($this->employeSpies->contains($employeSpy)) {
$this->employeSpies->removeElement($employeSpy);
$employeSpy->removeClientEmploye($this);
}
return $this;
}
}
and my table EmployeSpie:
class EmployeSpie extends User
{
/**
* #ORM\ManyToMany(targetEntity=Client::class, inversedBy="employeSpies")
*/
private $clients;
/**
* #return Collection|Client[]
*/
public function getClients(): Collection
{
return $this->clients;
}
public function addClient(Client $client): self
{
if (!$this->clients->contains($client)) {
$this->clients[] = $client;
}
return $this;
}
public function removeClient(Client $client): self
{
if ($this->clients->contains($client)) {
$this->clients->removeElement($client);
}
return $this;
}
public function __toString()
{
return $this->getPrenom()." ".$this->getNom();
}
My forms are made with a Symfony form:
class ClientType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('password')
->add('nom')
->add('prenom')
->add('telephone')
->add('fax')
->add('is_active')
->add('client_fonction')
->add('site')
->add('employeSpies', EntityType::class, array(
'class' => EmployeSpie::class ,
'label' => 'Sélectionnez les emloyés rattachés à ce client',
'expanded' => false,
'multiple' => true,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Client::class,
]);
}
}
and in my Controller I've made the following thing:
/**
* #Route("/admin/clients/create", name="admin.client.new")
* #param Request $request
* #return RedirectResponse|Response
*/
public function new(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$client = new Client();
$form = $this->createForm(ClientType::class, $client);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$client->setRoles(array('ROLE_CUSTOMER'));
$client->setPassword(
$passwordEncoder->encodePassword(
$client,
$form->get('password')->getData()
)
);
$this->em->persist($client);
$this->em->flush();
$this->addFlash('success', 'Nouveau client crée avec succès');
$this->redirectToRoute('admin.clients.index');
}
return $this->render("admin/clients/create.html.twig", [
'client' => $client,
'form' => $form->createView()
]);
}
/**
* #Route("/admin/clients/{id}", name="admin.client.edit", methods="GET|POST")
* #param Client $client
* #return Response
*/
public function edit(Client $client,Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$form = $this->createForm(ClientType::class, $client);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$clientEmploye = $request->request->get('client');
$clientEmploye = $clientEmploye['employeSpies'];
$client->setPassword(
$passwordEncoder->encodePassword(
$client,
$form->get('password')->getData()
)
);
foreach ($form->get('employeSpies')->getData() as $employe){
$client->addEmployeSpy($employe);
}
$client->setRoles(array('ROLE_CUSTOMER'));
$this->em->flush();
$this->addFlash('success', 'Nouveau client modifié avec succès');
$this->redirectToRoute('admin.clients.index');
}
return $this->render("admin/clients/edit.html.twig", [
'client' => $client,
'form' => $form->createView()
]);
}
Si my user is created or edited normally but I did not store the link for employeSpies in my form. Do you have any idea why?

I found the answer to my problem.
#Jakumi was right but few other cha ges were needed.
In my client Entity I has to change :
public function addEmployeSpy(EmployeSpie $employeSpy): self
{
if (!$this->employeSpies->contains($employeSpy)) {
$this->employeSpies[] = $employeSpy;
$employeSpy->addClientEmploye($this);
}
return $this;
}
to :
public function addEmployeSpy(EmployeSpie $employeSpy): self
{
if (!$this->employeSpies->contains($employeSpy)) {
$this->employeSpies[] = $employeSpy;
$employeSpy->addClient($this);
}
return $this;
}
Same thing for the remove.
public function removeEmployeSpy(EmployeSpie $employeSpy): self
{
if ($this->employeSpies->contains($employeSpy)) {
$this->employeSpies->removeElement($employeSpy);
$employeSpy->removeClientEmploye($this);
}
return $this;
}
to :
public function removeEmployeSpy(EmployeSpie $employeSpy): self
{
if ($this->employeSpies->contains($employeSpy)) {
$this->employeSpies->removeElement($employeSpy);
$employeSpy->removeClient($this);
}
return $this;
}
But after the other change in my ClientType :
->add('employeSpies', EntityType::class, array(
'class' => EmployeSpie::class ,
'by_reference' => false,
'label' => 'Sélectionnez les employés rattachés à ce client',
'expanded' => false,
'multiple' => true,
))
I need to add the 'by_reference' => false,to make it works.
Because of this Symfony will not try to find the "setClient" method but to find addClient method
Hope it could help later some other persons :)

Related

How to generate FormType for many-to-many relation for non-existing items in Symfony 6?

I have simple many-to-many relation between Article and Tag entities. I want to create a new article using FormType and associate tags with it. But the case is: I want to associate tags that may not exist yet.
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class)
->add('tags', EntityType::class, [
'class' => Tag::class,
'multiple' => true
])
;
}
This FormType generates a multi select form for existing tags only. But I want to have a <textarea> field, where users can put existing and not existing tags. Then after form submission, existing tags would be associated with the new article, and not existing tags first would be added and then associated with a new article.
I'm pretty new in Symfony world, so excuse me if my problem is trivial.
First, sorry for my English. I will give you an example based on Embarazada and EtiquetaAspectoEmbarazada entities (Many to many relationship) and the use of tetranz /select2entity-bundle The idea of that is register an pregnant woman and asociate her many tags, in case of entered text a tag name that not exist, it could be inserted from the Embarazada form type.
EmbarazadaType form class sumarized:
namespace App\Form;
use App\Entity\EtiquetaAspectoEmbarazada;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Tetranz\Select2EntityBundle\Form\Type\Select2EntityType;
class EmbarazadaType extends AbstractType
{
public function __construct(private readonly ManagerRegistry $em, private readonly array $formParams)
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nombre', TextType::class, [
'label' => 'Nombre y Apellidos*',
])
->add('otrosAspectos', Select2EntityType::class, [
'label' => 'Aspectos de interés',
'label_attr' => ['class' => 'text-secondary'],
'class' => EtiquetaAspectoEmbarazada::class,
'remote_route' => 'embarazada_encontrarEtiquetaAspectos', // an action to search tags by text typed.
'primary_key' => 'id',
'text_property' => 'text',
'multiple' => true,
'allow_clear' => false,
'delay' => 250,
'cache' => false,
'minimum_input_length' => 3,
'scroll' => true,
'page_limit' => $this->formParams['limite_resultados_etiquetas'],
'language' => 'es',
'width' => '100%',
'placeholder' => '',
'allow_add' => [
'enabled' => true,
'new_tag_text' => '(NUEVA)',
'new_tag_prefix' => '**',
'tag_separators' => '[",", ";", " "]'
],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Embarazada::class,
'attr' => ['id' => 'embarazadaForm', 'autocomplete' => 'off'],
]);
}
}
The logic of the action captarAction (register pregnam woman) look for the comments inside:
public function captarAction(Request $request, ManagerRegistry $manager, UuidEncoder $uuidEncoder, LoggerInterface $logger): Response
{
if ($request->isXmlHttpRequest()) {
try {
$nuevaEmbarazada = new Embarazada();
$form = $this->createForm(EmbarazadaType::class, $nuevaEmbarazada, [
'action' => $this->generateUrl('embarazadas_captar', ['cmfId' => $uuidEncoder->encode($estructuraOrganizativa->getIdPublico())]),
'method' => Request::METHOD_POST,
]);
if ($request->isMethod(Request::METHOD_POST)) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$conn = $em->getConnection();
$conn->beginTransaction();
try {
/**
* All tags present in the collection otrosAspectos with empty id, are takens as news, so need to remove '**' that indicate
* in that case the end of a new tag and persist, checking first if not exist in database another tag with similar name without '**'.
**/
foreach ($nuevaEmbarazada->getOtrosAspectos() as $etiqueta) {
if (empty($etiqueta->getId())) {
$etiquetaSimilar = $manager->getRepository(EtiquetaAspectoEmbarazada::class)
->findOneBy(['text' => trim((string) $etiqueta->getText(), "/*+/")]);
// find similar tag without '**' symbol, if exist replace in the associated collection.
if (!\is_null($etiquetaSimilar)) {
$nuevaEmbarazada->getOtrosAspectos()->removeElement($etiqueta); // remove the original tag with '**' and insert the new one without them.
$nuevaEmbarazada->addOtrosAspecto($etiquetaSimilar);
} else {
// if not exist, persist a new tag without '**' symbol.
$etiqueta->setText(trim((string) $etiqueta->getText(), "/*+/"));
$em->persist($etiqueta);
}
} else {
continue;
}
}
$em->persist($nuevaEmbarazada);
$em->flush();
$conn->commit();
} catch (Exception $exc) {
$conn->rollback();
$conn->close();
// return user friendly errors
}
return new Response("La embarazada fue registrada satisfactoriamente.");
} else {
$form->addError(new FormError("Error al registrar la embarazada. Rectifique los errores señalados en cada sección."));
return new Response($this->renderView('Embarazadas/frmCaptarEmbarazada.html.twig', ['form' => $form->createView(), 'enEdicion' => false, 'edadGestacionalActual' => $nuevaEmbarazada->getEdadGestacional()]), Response::HTTP_NOT_ACCEPTABLE);
}
}
return $this->render('Embarazadas/frmCaptarEmbarazada.html.twig', ['form' => $form->createView(), 'enEdicion' => false, 'edadGestacionalActual' => $nuevaEmbarazada->getEdadGestacional()]);
} catch (\Exception $exc) {
$logger->error(sprintf("[%s:%s]: %s", self::class, __FUNCTION__, $exc->getMessage()));
return new Response("Ocurrió un error inesperado al ejecutar la operación", Response::HTTP_INTERNAL_SERVER_ERROR);
}
} else {
throw $this->createNotFoundException("Recurso no encontrado");
}
}
Check the final result (red frame):
You should use several tricks to reach it.
Put to options choices all existing tags but as a relation ArticleHasTag.
Write some logic for deleting orphan entities on submit action.
I can show you how it works.
look at choices option and submit the fragment here.
#[Route(path: '/article/{articleId}', methods: ['GET', 'POST'])]
public function articleEdit(string $articleId, Request $request): Response
{
$article = $this->entityManager->find(Article::class, $articleId);
if (!$article) {
throw new NotFoundHttpException();
}
$originalTags = new ArrayCollection($article->getArticleHasTagList()->toArray());
$formBuilder = $this->createFormBuilder($article, [
'data_class' => Article::class,
]);
$formBuilder->add('title', TextType::class);
$formBuilder->add('articleHasTagList', EntityType::class, [
'class' => ArticleHasTag::class,
'choice_label' => 'tag.name',
'choice_value' => 'tag.id',
'multiple' => true,
'choices' => (function (Article $article) {
$articleHasTagList = clone $article->getArticleHasTagList();
$tags = $this->entityManager->getRepository(Tag::class)->findAll();
foreach ($tags as $tag) {
/** #var ArticleHasTag[] $articleHasTagList */
foreach ($articleHasTagList as $articleHasTag) {
if ($tag === $articleHasTag->getTag()) {
continue 2;
}
}
$articleHasTag = new ArticleHasTag();
$articleHasTag->setArticle($article);
$articleHasTag->setTag($tag);
$articleHasTagList->add($articleHasTag);
}
return $articleHasTagList;
})($article),
]);
$formBuilder->add('submit', SubmitType::class);
$form = $formBuilder->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($article);
/** #var ArticleHasTag[] $originalTags */
foreach ($originalTags as $articleHasTag) {
if (!$article->getArticleHasTagList()->contains($articleHasTag)) {
$this->entityManager->remove($articleHasTag);
}
}
foreach ($article->getArticleHasTagList() as $articleHasTag) {
$this->entityManager->persist($articleHasTag);
}
$this->entityManager->flush();
return $this->redirectToRoute('app_app_articleedit', [
'articleId' => $articleId,
]);
}
return $this->render('base.html.twig', [
'form' => $form->createView(),
]);
}
of course, you also should make entities.
// src/Entity/Article.php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
#[Entity]
class Article
{
#[Id]
#[Column(type: 'bigint')]
#[GeneratedValue]
private string $id;
#[Column]
private string $title;
/**
* #var Collection<int, ArticleHasTag>
*/
#[OneToMany(mappedBy: 'article', targetEntity: ArticleHasTag::class, fetch: 'EAGER')]
private Collection $articleHasTagList;
public function __construct()
{
$this->articleHasTagList = new ArrayCollection();
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function addArticleHasTagList(ArticleHasTag $articleHasTag): void
{
$articleHasTag->getTag()->addTagHasArticleList($articleHasTag);
$articleHasTag->setArticle($this);
$this->articleHasTagList->add($articleHasTag);
}
public function removeArticleHasTagList(ArticleHasTag $articleHasTag): void
{
$articleHasTag->getTag()->removeTagHasArticleList($articleHasTag);
$this->articleHasTagList->removeElement($articleHasTag);
}
public function getArticleHasTagList(): Collection
{
return $this->articleHasTagList;
}
}
// src/Entity/ArticleHasTag.php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\UniqueConstraint;
#[Entity]
#[UniqueConstraint(name: 'uniqArticleIdTagId', columns: ['article_id', 'tag_id'])]
#[Index(columns: ['article_id'], name: 'idxArticleId')]
#[Index(columns: ['tag_id'], name: 'idxTagId')]
class ArticleHasTag
{
#[Id]
#[Column(type: 'bigint')]
#[GeneratedValue]
private string $id;
#[ManyToOne(targetEntity: Article::class, fetch: 'EAGER', inversedBy: 'articleHasTagList')]
#[JoinColumn(nullable: false)]
private Article $article;
#[ManyToOne(targetEntity: Tag::class, fetch: 'EAGER', inversedBy: 'tagHasArticleList')]
#[JoinColumn(nullable: false)]
private Tag $tag;
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getArticle(): Article
{
return $this->article;
}
public function setArticle(Article $article): void
{
$this->article = $article;
}
public function getTag(): Tag
{
return $this->tag;
}
public function setTag(Tag $tag): void
{
$this->tag = $tag;
}
}
// src/Entity/Tag.php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
#[Entity]
class Tag
{
#[Id]
#[Column(type: 'bigint')]
#[GeneratedValue]
private string $id;
#[Column]
private string $name;
/**
* #var Collection<int, ArticleHasTag>
*/
#[OneToMany(mappedBy: 'tag', targetEntity: ArticleHasTag::class, fetch: 'EAGER')]
private Collection $tagHasArticleList;
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function __construct()
{
$this->tagHasArticleList = new ArrayCollection();
}
public function addTagHasArticleList(ArticleHasTag $articleHasTag): void
{
if (!$this->tagHasArticleList->contains($articleHasTag)) {
$this->tagHasArticleList->add($articleHasTag);
}
}
public function removeTagHasArticleList(ArticleHasTag $articleHasTag): void
{
if ($this->tagHasArticleList->contains($articleHasTag)) {
$this->tagHasArticleList->removeElement($articleHasTag);
}
}
public function getTagHasArticleList(): Collection
{
return $this->tagHasArticleList;
}
}

symfony submitting to link table entity

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.

Embedding a single object in form

I've manage to create a form embedded in another form but I think I'm not doing something right. Here's my code
Category
class Category
{
private $id;
private $name;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="category")
*/
private $subcategorues;
public function __construct()
{
$this->subcategorues = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function addSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
$this->subcategorues[] = $subcategorues;
return $this;
}
public function removeSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
$this->subcategorues->removeElement($subcategorues);
}
public function getSubcategorues()
{
return $this->subcategorues;
}
}
Subcategory
class Subcategory
{
private $id;
private $name;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="subcategories")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param mixed $category
*/
public function setCategory($category)
{
$this->category = $category;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
}
CategoryType
.......
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'entity', [
'class' => 'AppBundle\Entity\Category',
'choice_label' => 'name'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Category'
]);
}
......
SubcategoryType
$builder
->add('category', new CategoryType(), [
'label' => false
])
->add('name', 'text')
->add('save', 'submit')
;
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Subcategory'
]);
}
DefaultController
public function indexAction(Request $request)
{
$subcategory = new Subcategory();
$form = $this->createForm(new SubcategoryType(), $subcategory);
$form->handleRequest($request);
if($form->isValid()){
$em = $this->getDoctrine()->getManager();
$subcategory->setCategory($subcategory->getCategory()->getName());
$em->persist($subcategory);
$em->flush();
return new Response(sprintf('ID %d', $subcategory->getId()));
}
return $this->render('AppBundle::layout.html.twig', [
'form' => $form->createView(),
]);
}
Please notice this line of code $subcategory->setCategory($subcategory->getCategory()->getName());
I need that line in order to save the entity to the database otherwise I get an error. So my question is is there a way to skip this line of code and pass category object on the fly to subcategory->category property instead of doing that manually?
//EDIT
Here's the output of dump($form->getData());
DefaultController.php on line 33:
Subcategory {#467 ▼
-id: null
-name: "Uncharted"
-category: Category {#588 ▼
-id: null
-name: Category {#685 ▼
-id: 2
-name: "Games"
-subcategorues: PersistentCollection {#686 ▶}
}
-subcategorues: ArrayCollection {#660 ▶}
}
}
Your CategoryType is not correctly mapped compared to your Category entity. Actually, in your case, you don't need to have a sub-form CategoryType with a name field, since you have a category field in SubCategory which is a relationship towards Category.
Just replace:
->add('category', new CategoryType(), [
'label' => false
])
by:
->add('category', 'entity', [
'class' => 'AppBundle\Entity\Category',
'choice_label' => 'name'
]);
Could your try smth like this (for Category entity class):
public function addSubcategorue(\AppBundle\Entity\Category $subcategorues)
{
if ($this->subcategorues->contains($subcategorues)) {
$this->subcategorues->add($subcategorues);
$subcategorues->setCategory($this);
}
return $this;
}

Symfony2 DataTransformer after handleRequest

I have an API and I am sending a reference of an entity, I'm using a DataTransformer to get my entity but the DataTransformer is always called before the $form->handleRequest($request) the value is always null and it could't works
My Controller
public function newAction(Request $request)
{
$orderNewFormType = $this->get('competitive_bo.api_bundle.form.type.order_new');
$card = new Card();
try {
$form = $this->createForm($orderNewFormType, $card);
$form->handleRequest($request);
} catch (TransformationFailedException $e) {
return $this->notFoundErrorResponse(
'Business not found'
);
}
if ($form->isValid()) {
return $this->okResponse(array());
}
$validatorErrorFormatter = $this->get('competitive_bo.api_bundle.formatter.validator_error');
$errors = $validatorErrorFormatter->formatFromFormError($form->getErrors(true));
return $this->badRequestErrorResponse(
'Invalid data',
$errors
);
}
The form type
class OrderNewFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('customer', 'entity', array(
'class' => 'CompetitiveBOBusinessBundle:Customer',
'property' => 'id'
))
->add('business', 'business', array(
'mapped' => false,
))
;
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => Card::class
));
}
public function getName()
{
return null;
}
}
The Business form type
class BusinessReferenceFormType extends AbstractType
{
/**
* #var ReferenceToBusinessTransformer
*/
private $referenceToBusinessTransformer;
public function __construct(ReferenceToBusinessTransformer $referenceToBusinessTransformer)
{
$this->referenceToBusinessTransformer = $referenceToBusinessTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->referenceToBusinessTransformer);
}
public function getName()
{
return 'business';
}
public function getParent()
{
return 'text';
}
}
And the DataTransformer
/**
* Class ReferenceToBusinessTransformer
*/
class ReferenceToBusinessTransformer implements DataTransformerInterface
{
/**
* #var BusinessRepository
*/
private $businessRepository;
public function __construct(BusinessRepository $businessRepository)
{
$this->businessRepository = $businessRepository;
}
/**
* {#inheritdoc}
*/
public function transform($reference)
{
var_dump($reference);
$business = $this->businessRepository->findOneBy(array(
'reference' => $reference
));
if (null === $business) {
throw new TransformationFailedException;
}
return $business;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($value)
{
if (!($value instanceof Business)) {
throw new TransformationFailedException;
}
return $value->getReference();
}
}
The var_dump($reference) is always null
And I have my test
public function testNewAction($getParams, $postParam, $responseCode)
{
$client = static::createClient();
$router = $client->getContainer()->get('router');
$route = $router->generate('competitivebo_api_order_new',$getParams);
$client->request('POST', $route, $postParam);
$response = $client->getResponse();
$this->assertJsonResponse($response, $responseCode);
}
With the post params
'customer' => 1,
'business' => LoadBusinessData::REFERENCE_1,
'name' => 'Test',
The exception TransformationFailedException is always thrown during the $this->createForm(...) so the request is not handled
According with the documentation,
When null is passed to the transform() method, your transformer should return an equivalent value of the type it is transforming to (e.g. an empty string, 0 for integers or 0.0 for floats).

Form edition for a N:M relationship with extra fields is not working

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.

Categories