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
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;
}
]);
}
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 %}
I would like to create a google categories matching(first field categorie from database and second field a user autocomplete field from google categories) form where i have an entity CategoriesConfig :
private $id;
/**
* #var string
*
* #ORM\Column(name="category_site", type="string", length=100)
*/
private $categorySite;
/**
* #var string
*
* #ORM\Column(name="category_google", type="string", length=100)
*/
private $categoryGoogle;
In my Controller i tried this
/**
* #Route("/adminDashboard/categoriesMatching", name="googleShopping_categories")
* #Security("has_role('ROLE_SUPER_ADMIN')")
*/
public function categoriesMatchingAction(Request $request)
{
// create a task and give it some dummy data for this example
$idSite = $this->get('session')->get('_defaultWebSite')->getId();
$categories = $this->getDoctrine()->getRepository('DataSiteBundle:SiteCategory')->findBy(array('IdSite' => $idSite));;
$categories_config = new CategoriesConfig();
//var_dump($categories);exit;
$form = $this->createForm(new CategoriesConfigType($categories), $categories_config);
return $this->render('GoogleShoppingBundle:Default:categoriesMatching.html.twig', array(
'form' => $form->createView()
));
}
And my form type : CategoriesConfigType:
class CategoriesConfigType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
private $site_categories;
public function __construct ($site_categories) {
$this->site_categories = $site_categories;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($this->site_categories as $k => $categorie){
$builder
->add('categorySite')
->add('categoryGoogle');
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
I would like to have as many categories rows as row fields(website itecategorie and google categorie)
The result is like that:
Thank you in advance!
Your loop on $this->categories is uneffective, because the elements you add have the same name each time (categorySite and categoryGoogle), so the FormBuilder replaces the form field each time, instead of adding another one.
However, if you want your form to handle a Collection of CategoryConfigs, you need to take a different approach.
1) Create a CategoriesConfigType (as you did), but who is responsible of only a single CategoriesConfig entity
class CategoriesConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('categorySite')
->add('categoryGoogle');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sp\GoogleShoppingBundle\Entity\CategoriesConfig'
));
}
}
2) Then use CollectionType field to manipulate your form as a whole collection of CategoryConfigTypes:
class YourCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('categoriesConfigs', CollectionType::class, array(
'entry_type' => CategoriesConfigType::class,
'entry_options' => array('required' => false)
);
}
}
I have some entities that share the same structure, for example, take this two as a base example:
/**
* #ORM\Entity
* #ORM\Table(name="nomencladores.modelo", schema="nomencladores")
*/
class Modelo
{
use IdentifierAutogeneratedEntityTrait;
use NamedEntityTrait;
use ActiveEntityTrait;
/**
* #var \TipoTramite
*
* #ORM\ManyToOne(targetEntity="TipoTramite")
* #ORM\JoinColumn(name="tipo_tramite_id", referencedColumnName="id")
*/
protected $tipo_tramite;
/**
* Set tipo tramite
*
* #param \ComunBundle\Entity\TipoTramite $tipo_tramite
* #return FabricanteDistribuidor
*/
public function setTipoTramite(\ComunBundle\Entity\TipoTramite $tipo_tramite)
{
$this->tipo_tramite = $tipo_tramite;
return $this;
}
/**
* Get tipo tramite
*
* #return \ComunBundle\Entity\TipoTramite
*/
public function getTipoTramite()
{
return $this->tipo_tramite;
}
}
/**
* #ORM\Entity
* #ORM\Table(name="nomencladores.marca", schema="nomencladores")
*/
class Marca
{
use IdentifierAutogeneratedEntityTrait;
use NamedEntityTrait;
use ActiveEntityTrait;
/**
* #var \TipoTramite
*
* #ORM\ManyToOne(targetEntity="TipoTramite")
* #ORM\JoinColumn(name="tipo_tramite_id", referencedColumnName="id")
*/
protected $tipo_tramite;
/**
* Set tipo tramite
*
* #param \ComunBundle\Entity\TipoTramite $tipo_tramite
* #return FabricanteDistribuidor
*/
public function setTipoTramite(\ComunBundle\Entity\TipoTramite $tipo_tramite)
{
$this->tipo_tramite = $tipo_tramite;
return $this;
}
/**
* Get tipo tramite
*
* #return \ComunBundle\Entity\TipoTramite
*/
public function getTipoTramite()
{
return $this->tipo_tramite;
}
}
As you can see in the code above the entities share almost the same code, just change the table where information is store. Now I need to build a form for each of them and basically will be the same with some minor changes, see this example for Modelo entity:
class ModeloType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nombre')
->add('activo')
->add('tipo_tramite', 'entity', array(
'class' => 'ComunBundle:TipoTramite',
'property' => 'nombre',
'empty_value' => '-- SELECCIONAR --',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('qb')
->where('qb.activo = :activoValue')
->setParameter('activoValue', TRUE);
}
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'ComunBundle\Entity\Modelo'
));
}
public function getName()
{
return 'Modelo';
}
}
So, the only things changing between one FormType and the other will be: the class name, the data_class attribute and the getName() return value, is there any way to apply DRY on FormType on this scenario?
Firstly, having two almost identical Entities (and hence two almost identical tables) seems like a smell to me - I'd think hard about somehow combining them into one Entity with some additional category to distinguish them.
That aside, can you just define your own intermediate abstract type, and then inherit from that changing those two small things that are different, e.g.
//This is not a good name, I'm sure you can do better!
abstract class AbstractModeloMarcaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nombre')
->add('activo')
->add('tipo_tramite', 'entity', array(
'class' => 'ComunBundle:TipoTramite',
'property' => 'nombre',
'empty_value' => '-- SELECCIONAR --',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('qb')
->where('qb.activo = :activoValue')
->setParameter('activoValue', TRUE);
}
));
}
abstract protected function getDataClass();
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => $this->getDataClass()
));
}
}
public class ModeloType extends AbstractModeloMarcaType
{
public function getName()
{
return 'Modelo';
}
public function getDataClass()
{
return 'ComunBundle\Entity\Modelo';
}
}
public class MarcaType extends AbstractModeloMarcaType
{
public function getName()
{
return 'Marca';
}
public function getDataClass()
{
return 'ComunBundle\Entity\Marca';
}
}
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.