Symfony2: Saving a collection entity confusion - symfony

The disclaimer: I'm new to Symfony. Really struggling with the collection field type and simple setup of a OnetoOne relationship.
Scenario: I have a Product entity class and a Category entity class. I am using a collection field on the product to create Category items. I am picturing a separate Category Table with a Name column and a related product_id column. I know it's not typically practical. For arguments sake Category may as well be Feature or something else as I just want to establish the relationship as a scenario to extend. Ultimately Category will become an Image field allowing me to pull related images into a view.
The Problem: I've followed the cookbook article (http://symfony.com/doc/current/cookbook/form/form_collections.html) over and over but just hitting brick wall. I'm on board with the principle but feel like I'm missing something significant. I've got a protoype form field generating from the javascript and I'm successfully persisting/saving new Products (in full) and new Categories (only in part). The related product id is not being written into the join column.
I'm sure it's a case of getters/setters not being taken care of correctly. I' relying on doctrine to generate them automatically. Or the issue may be with some unspecified requirement to set the id to the Category in the controller.
Code to follow. Help greatly appreciated as been banging this around for a couple of days and getting nowhere fast. Really frustrating as grasped all other principles really quickly and really chuffed with building a first project in symfony.
Product Entity
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\Category;
/**
* Page
*
* #ORM\Table(name="product")
* #ORM\Entity
* #Vich\Uploadable
*/
class Product
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="Title", type="string", length=255)
* #Assert\NotBlank()
*/
private $title;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #ORM\OnetoOne(targetEntity="Category", cascade={"persist"})
*/
protected $categorys;
public function __construct()
{
$this->categorys = new ArrayCollection();
}
}
Category Entity
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Product;
/**
* Category
*
* #ORM\Table(name="category")
* #ORM\Entity
*/
class Category
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\OneToOne(targetEntity="Product", cascade={"persist"})
*/
protected $product;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set product
*
* #param \AppBundle\Entity\Product $product
* #return Category
*/
public function setProduct(\AppBundle\Entity\Product $product = null)
{
$this->product = $product;
return $this;
}
/**
* Get product
*
* #return \AppBundle\Entity\Product
*/
public function getProduct()
{
return $this->product;
}
}
Product Type
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Entity\Product;
use AppBundle\Entity\Category;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text')
->add('categorys', 'collection', array(
'type' => new CategoryType(),
'allow_add' => true,
'by_reference' => false,
))
->add('save', 'submit', array(
'attr' => array('class' => 'btn btn-default'),
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product',
));
}
public function getName()
{
return 'product';
}
}
Category Type
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Entity\Category;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Category',
));
}
public function getName()
{
return 'category';
}
}
Product Controller: New Product
/**
* #Route("admin/product/new", name="product_add")
* #Security("has_role('ROLE_ADMIN')")
*/
public function newAction(Request $request)
{
$product = new Product();
$form = $this->createForm(new ProductType(), $product);
$category = new Category();
$form->handleRequest($request);
if ($form->isValid()) {
$category->getProduct($this);
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
return $this->redirectToRoute('products_admin');
}
return $this->render('Product/productAdd.html.twig', array(
'form' => $form->createView(),
));
}

I bet that you don't need OneToOne but OneToMany relation for Product vs Category. Because i think that one category can have multiple products but a product can only have one category.
Another approach is even to make a ManyToMany relation. In that case each category can have multiple products, but each product can be in multiple categories too.
OneToOne is not often used, only in special circumstances.

I think you dont need the collection-type too in your case. You could change this for the entity type. Just make the entities ManyToOne as i explained.
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Product
*
* #ORM\Table()
* #ORM\Entity
*/
class Product
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=64)
*/
private $description;
/**
* #var float
*
* #ORM\Column(name="price", type="float")
*/
private $price;
/**
* #ORM\ManyToOne(targetEntity="Category")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
**/
private $category;
}
and category:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Category
*
* #ORM\Table()
* #ORM\Entity
*/
class Category
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=64)
*/
private $name;
public function __toString() {
return $this->name;
}
}
and now use the console to run some commands:
app/console doctrine:generate:entities AppBundle
app/console doctrine:schema:update --force
app/console doctrine:generate:crud AppBundle:Product
app/console doctrine:generate:crud AppBundle:Category
choose YES if asked if you want to generate write Actions
Add a __toString() method to your Category entity:
public function __toString() {
return $this->name;
}
Now see your new controllers and routes and try them.
To see all your routes:
app/console router:debug
You must have this in your app/config/routing.yml:
app:
resource: "#AppBundle/Controller/"
type: annotation
At the end you will have:
- two new entities
- two new controllers
- two new form types
- eight new views
You can learn a lot from the created code and change everything as you wish.
Good luck

I know it's a old question, but :
/**
* #Route("admin/product/new", name="product_add")
* #Security("has_role('ROLE_ADMIN')")
*/
public function newAction(Request $request)
{
$product = new Product();
$form = $this->createForm(new ProductType(), $product);
$category = new Category();
$form->handleRequest($request);
if ($form->isValid()) {
#################################
##$category->getProduct($this);##
#################################
$category->setProduct($this);
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
return $this->redirectToRoute('products_admin');
}
return $this->render('Product/productAdd.html.twig', array(
'form' => $form->createView(),
));
}
You call getProduct($this) instead of setProduct($this)

Related

What is the most efficient way to conduct a search into a OneToMany relationship using doctrine on Symfony 4?

I have two entities (User and Product) with a OneToMany relationship. I want to retrieve all Product entities related to a specific User filtering them by a field called "finished". I will post the relevant information of those entities:
User Entity:
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="user")
*/
private $products;
....
Product Entity:
/**
* #ORM\Entity(repositoryClass="App\Repository\Productepository")
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="products")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="boolean")
*/
private $finished;
....
I am using the following code to filter my products, but is there any other more efficient way to do it?
$user = $this->getUser();
$products = $user->getProducts();
$filtered = [];
foreach ($products as $product){
if(!$product->getFinished()){
$filtered[] = $product;
}
}
Thanks for your help
One of the solutions would be to use a database query like the following :
- in the controller, you can do
<?php
namespace App\Controller\ProductController;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
class ProductController extends AbstractController
{
/**
* #Route("/products", methods={"GET"}, name="app_get_products")
* #IsGranted("IS_AUTHENTICATED_REMEMBERED")
*/
public function listProduct(ProductRepository $repository)
{
$user = $this->getUser();
$products = $repository->findBy([
'user' => $user,
'finished' => false
]);
return $this->render('Product/list.html.twig', [
'products' => $products,
]);
}
}
Notice the request that is made : this is an attempt at a solution.

Symfony 3.3.16 Doctrine Relations & Dictonary tables for fast data processing with Foms based on EntityType, can't pass choiced value to database

it's not a typical approach where "user adds article". This approach is a try to build nicely configurable, and data validated (by forms on frontend) table in database contains data based from other tables - something I'm trying to call "Dictonary tables", and store ID in main table of of vchar representation in other tables
This approach gives me a table which is very fast to process data on it.
Unfortunately, I encountered some difficulties using EntityType in forms
Adding ORM relation to entity (ConfigTable.php) causes writing null into database (POST have all values properly seted)
UPDATE 2018.02.15
annotation
#ORM\ManyToOne(targetEntity="AppBundle\Entity\yourentity", inversedBy="mappedByFieldName")
#ORM\JoinColumn(name="field_in_current_entity_declared_as_ORM#/Column")
protected $entityOne
#ORM\JoinColum is nt working properly here. Setting "name" attribute pointing to existing field can't create proper foreign keys.
If JoinColumn is not setted at all - FK are pointing to your current annotated field wits suffix _id
ie entity_one_id
and variables are saved from form,
but if in JoinColumn you set *name="pointing_to_field_annotaded_as_column_for_doctrine"*
during saving data to database you will get error of that object
*YourBundle/Entity/EntityName - cannot be converted to INT*
DefaultController.php
/**
* #param Request $request
* #Route("/add", name="action_configtable_add")
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function addFieldConfigTableAction(Request $request)
{
$form = $this->createForm(ConfigTableFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($data);
$em->flush();
return $this->redirectToRoute('homepage');
}
return $this->render(
':Forms/ConfigTable:add.config.table.item.html.twig',
[
'cdata' => $form->createView(),
]
);
}
MainEntity - in which I would like to store ID from dictonary entities (DictonaryOne.php, OtherDictonary.php)
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ConfigTable
* #package AppBundle\Entity
* #ORM\Entity(repositoryClass="AppBundle\Repository\ConfigTableRepository")
* #ORM\Table(name="config_table")
*/
class ConfigTable
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #ORM\Column(type="integer", nullable=true )
*/protected $dictonaryOne;
/**
* #ORM\Column(type="integer", nullable=true )
*/protected $dictonaryTwo;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
* #ORM\JoinColumn(name="dictonary_one")
*/
protected $dictOne;
//usual getters & setters
}
DictonaryOne Entity (binded by relation, cant save it's id into database in ConfigTable)
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class DictonaryOne
* #package AppBundle\Entity
* #ORM\Entity(repositoryClass="AppBundle\Repository\DictonaryOneRepository")
* #ORM\Table(name="dictonary_one")
*/
class DictonaryOne
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #var string
* #ORM\Column(type="string", nullable=true)
*/
protected $name;
/**
* #var string
* #ORM\Column(type="string", nullable=true)
*/
protected $description;
/**
* #var boolean
* #ORM\Column(type="boolean", options={"default":1})
*/
protected $isActive;
/**
* #var int
* #ORM\Column(type="integer", options={"default":"1"})
*/
protected $orderField;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\ConfigTable", mappedBy="dictOne")
*/
protected $configTableOne;
public function __construct()
{
$this->configTableOne = new ArrayCollection();
}
//usual getters & setters and:
/**
* #return mixed
*/
public function getConfigTableOne()
{
return $this->configTableOne;
}
/**
* #param mixed $configTableOne
*/
public function setConfigTableOne(ConfigTable $configTableOne)
{
$this->configTableOne = $configTableOne;
}
}
OtherDictonary Entity: (not binded by relation, data are stored in database corectly after submit)
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class OtherDictonary
* #package AppBundle\Entity
* #ORM\Entity()
* #ORM\Table(name="other_dictonary")
*/
class OtherDictonary
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/protected $id;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $name;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $description;
/**
* #var boolean
* #ORM\Column(type="boolean")
*/
protected $isActive;
/**
* #var int
* #ORM\Column(type="integer")
*/
protected $orderField;
//usual getters & setters
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
public function __toString()
{
return (string)$this->id;
}
}
ConfigTableFormType.php
namespace AppBundle\Form;
use AppBundle\Entity\DictonaryOne;
use AppBundle\Entity\OtherDictonary;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ConfigTableFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'dictonaryOne',
EntityType::class,
[
'class'=> DictonaryOne::class,
'label' => 'label from dictonary 1',
'choice_value' => 'id',
'choice_label' => 'description',
'expanded' => false,
'multiple' => false,
// 'mapped' => false, //not working
]
)
->add(
'dictonaryTwo',
EntityType::class,
[
'class' => OtherDictonary::class,
'label' => 'label from other dictonary ',
'choice_value' => 'id',
'choice_label' => 'description',
'query_builder' => function (\Doctrine\ORM\EntityRepository $er) {
return $er->createQueryBuilder('d2')
->andWhere('d2.isActive = 1')
->orderBy('d2.description', 'ASC');
},
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => 'AppBundle\Entity\ConfigTable',
]
);
}
public function getBlockPrefix()
{
return 'app_bundle_config_table_form_type';
}
}
in ORM annotation, reference JoinColumn(name="xxx") is pointless.
try don't use notation:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
* #ORM\JoinColumn(name="dictonary_one")
*/
protected $dictOne;
use:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\DictonaryOne", inversedBy="configTableOne", cascade={"persist"})
*/
protected $dictOne;
foreign keys are formed well by symfony. Checked versions:
Symfony 2.8 - 3.3.16,
"doctrine/doctrine-bundle": "^1.6", (1.8.1)
"doctrine/orm": "^2.5", (v2.5.14)

Symfony2: Persisting entities connected via one-to-one relationship. FK field never set

I've spent hours on this issue and can't get it to work (new to Symfony2 and Doctrine2).
Scenario: I basically have 2 entities in a one-to-one relationship: Customer <-> Address. Just for the record: Customer and Address are using Doctrine's single table inheritance feature. So a Customer is always either a PrivateCustomer or a BusinessCustomer. "Address" is either an InvoiceAddress or a DeliveryAddress etc.... Maybe the problem is related to the STI.
Problem: I am using an embedded form for the InvoiceAddress entity within a Customer form so it gets created when a new customer is created. TRednering works fine, but when I am persisting/flushing the customer, a new customer gets created and a new invoice address, BUT the invoiceAddress customer_id is NULL, so the two never get conntected (the FK constraint fails of course). My understanding is that Doctrine will wrap everything in a transaction and automatically assign the customer_id. Am I wrong? It works if I manually persist the invoiceAddress before the customer, but I have the feeling that this should not be necessary.
Code (the relevant parts):
InvoiceAddress:
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
/**
* InvoiceAddress
*
* #ORM\Entity
*/
class InvoiceAddress extends Address
{
/**
* #var Entity\Customer
*
* #ORM\OneToOne(targetEntity="Entity\Customer", inversedBy="invoiceAddress")
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id", nullable=FALSE)
*/
private $customer;
/**
* #param Entity\Customer $customer
*/
public function setCustomer($customer)
{
$this->customer = $customer;
}
/**
* #return Entity\Customer
*/
public function getCustomer()
{
return $this->customer;
}
}
Customer:
namespace Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Validator\Constraints as Assert;
use Entity\User;
/**
* Customer
*
* #ORM\Entity(repositoryClass="Repository\CustomerRepository")
* #ORM\Table(name="customer")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"private" = "Entity\PrivateCustomer", "business" = "Entity\BusinessCustomer"})
*/
class Customer
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
.... OTHER FIELDS ......
/**
* #var \Entity\InvoiceAddress
*
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="Entity\InvoiceAddress", mappedBy="customer", cascade={"persist", "remove"}, orphanRemoval=true, fetch="EAGER")
*/
private $invoiceAddress;
/**
* #var \Entity\DeliveryAddress
*
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="DeliveryAddress", mappedBy="customer", cascade={"persist", "remove"}, orphanRemoval=true, fetch="LAZY")
*/
private $deliveryAddress;
/**
* #var \Entity\MarketingAddress
*
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="Entity\MarketingAddress", mappedBy="customer", cascade={"persist", "remove"}, orphanRemoval=true, fetch="LAZY")
*/
private $marketingAddress;
/**
* #var ArrayCollection Entity\Contact
*
* #Assert\Valid
*
* #ORM\OneToMany(targetEntity="Entity\Contact", mappedBy="customer", cascade={"persist", "remove"}, orphanRemoval=true, fetch="LAZY")
*/
private $contacts;
public function __construct()
{
$this->contacts = new ArrayCollection();
}
/**
* #return ArrayCollection
*/
public function getContacts()
{
return $this->contacts;
}
/**
* #param Contact $contact
*/
public function addContact(Contact $contact)
{
if (!$this->contacts->contains($contact)) {
$this->contacts->add($contact);
$contact->setCustomer($this);
}
}
/**
* #param Contact $contact
*/
public function removeContact(Contact $contact)
{
if ($this->contacts->contains($contact)) {
$this->contacts->removeElement($contact);
$contact->unsetCustomer();
}
}
/**
* #param Entity\MarketingAddress $marketingAddress
*/
public function setMarketingAddress($marketingAddress)
{
$this->marketingAddress = $marketingAddress;
}
/**
* #return Entity\MarketingAddress
*/
public function getMarketingAddress()
{
return $this->marketingAddress;
}
/**
* #param DeliveryAddress $deliveryAddress
*/
public function setDeliveryAddress($deliveryAddress)
{
$this->deliveryAddress = $deliveryAddress;
}
/**
* #return Entity\DeliveryAddress
*/
public function getDeliveryAddress()
{
return $this->deliveryAddress;
}
/**
* #param Entity\InvoiceAddress $invoiceAddress
*/
public function setInvoiceAddress($invoiceAddress)
{
$this->invoiceAddress = $invoiceAddress;
}
/**
* #return Entity\InvoiceAddress
*/
public function getInvoiceAddress()
{
return $this->invoiceAddress;
}
}
The action in CustomerController
/**
* Creates a new PrivateCustomer entity.
*
* #Route("/private", name="customer_create_private")
* #Method("POST")
* #Template("Customer:private_new.html.twig")
*/
public function createPrivateAction(Request $request)
{
$customer = new PrivateCustomer();
$form = $this->createCreateForm($customer);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($customer);
$em->flush();
return $this->redirect($this->generateUrl('customer_private_edit', array('id' => $customer->getId())));
}
return array(
'entity' => $customer,
'form' => $form->createView(),
);
}
In the CustomerType I defined the invoiceAddress field like so
$builder->add('invoiceAddress', new InvoiceAddressType(), array(
'label' => 'Rechnungsadresse',
'label_render' => false,
'widget_form_group_attr' => array(
'class' => 'form-inline'
)
));
Question: So do I have to manually do
$em->persist($customer->getInvoiceAddress())
$em->persist($customer)
just to set the FK. I thought Doctrine's magic should take care of that (because of the CASCADE="persist" option). Thx
class Customer
public function setInvoiceAddress($invoiceAddress)
{
$this->invoiceAddress = $invoiceAddress;
$invoiceAddress->setCustomer($this); // *** ADD THIS ***
}
It's a very common question. Just difficult to search for.

Embedded forms of inherited doctrine's entities

I'm building a form generator using Symfony 2.2 with Doctrine. The base principle is that the user create a new form by filling its name and selecting the widgets he'd likes to have in a select menu.
We can think of WidgetInputText, WidgetSelect, WidgetFile, etc.
Here is an example of my model:
<?php
namespace Ineat\FormGeneratorBundle\Entity\Widget;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Widget
*
* #ORM\Table(name="widget")
* #ORM\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"widget_text" = "WidgetText", "widget_input_text" = "WidgetInputText", "widget_select" = "WidgetSelect"})
*/
abstract class Widget
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Form", inversedBy="widgets")
*/
private $form;
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="Question")
*/
private $question;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set form
*
* #param \Ineat\FormGeneratorBundle\Entity\Form $form
* #return Widget
*/
public function setForm(\Ineat\FormGeneratorBundle\Entity\Form $form = null)
{
$this->form = $form;
return $this;
}
/**
* Get form
*
* #return \Ineat\FormGeneratorBundle\Entity\Form
*/
public function getForm()
{
return $this->form;
}
/**
* Set question
*
* #param \Ineat\FormGeneratorBundle\Entity\Question $question
* #return Widget
*/
public function setQuestion(\Ineat\FormGeneratorBundle\Entity\Question $question = null)
{
$this->question = $question;
return $this;
}
/**
* Get question
*
* #return \Ineat\FormGeneratorBundle\Entity\Question
*/
public function getQuestion()
{
return $this->question;
}
}
<?php
namespace Ineat\FormGeneratorBundle\Entity\Widget;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;
/**
* Widget
*
* #ORM\Entity
* #ORM\Table(name="widget_text")
*/
class WidgetText extends Widget
{
/**
* #var string
*
* #ORM\Column(type="text")
*/
private $text;
/**
* Set text
*
* #param string $text
* #return WidgetText
*/
public function setText($text)
{
$this->text = $text;
return $this;
}
/**
* Get text
*
* #return string
*/
public function getText()
{
return $this->text;
}
}
<?php
namespace Ineat\FormGeneratorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Form
*
* #ORM\Table(name="form")
* #ORM\Entity(repositoryClass="Ineat\FormGeneratorBundle\Entity\FormRepository")
* #UniqueEntity("name")
* #UniqueEntity("slug")
*/
class Form
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255)
*/
private $slug;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Widget", mappedBy="form", cascade={"persist"})
*/
private $widgets;
public function __construct()
{
$this->widgets = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Form
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set slug
*
* #param string $slug
* #return Form
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Get slug
*
* #return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* Add widgets
*
* #param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
* #return Form
*/
public function addWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
{
$this->widgets[] = $widget;
return $this;
}
/**
* Remove widgets
*
* #param \Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget
*/
public function removeWidget(\Ineat\FormGeneratorBundle\Entity\Widget\Widget $widget)
{
$this->widgets->removeElement($widget);
}
/**
* Get widgets
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getWidgets()
{
return $this->widgets;
}
/**
* Set widgets
*
* #param \Doctrine\Common\Collections\Collection $widgets
*/
public function setWidget(\Doctrine\Common\Collections\Collection $widgets)
{
$this->widgets = $widgets;
}
public function __set($name, $obj)
{
if (is_a($obj, '\Ineat\FormGeneratorBundle\Entity\Widget\Widget')) {
$this->addWidget($obj);
}
}
}
As you can see a form can have multiple widgets attached to him.
I've made an abstract class Widget because all widgets have common fields and are of type Widget and because in the Form entity it seems really really bad to had one collection per Widget type (bad and boring).
This model works, I've unit tested it and I'm able to attach a WidgetText to a form and then retrieve it.
The issue comes when I try to use forms with it.
<?php
namespace Ineat\FormGeneratorBundle\Form;
use Ineat\FormGeneratorBundle\Entity\Widget\WidgetText;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class FormType extends AbstractType
{
protected $widgets;
public function __construct(array $widgets = array())
{
$this->widgets = $widgets;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text')
->add('slug', 'text')
->add('WidgetText', 'collection', array(
'type' => new WidgetTextType(),
'allow_add' => true,
'attr' => array('class' => 'widget-text'),
'by_reference' => false
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Ineat\FormGeneratorBundle\Entity\Form',
));
}
public function getName()
{
return 'ineat_formgeneratorbundle_formtype';
}
}
<?php
namespace Ineat\FormGeneratorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WidgetTextType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', 'text')
;
}
public function getName()
{
return 'ineat_formgeneratorbundle_widgettexttype';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Ineat\FormGeneratorBundle\Entity\Widget\WidgetText',
));
}
}
When I try to display the form I've got the following error:
Neither property "WidgetText" nor method "getWidgetText()" nor method "isWidgetText()" exists in class "Ineat\FormGeneratorBundle\Entity\Form"
It's like Symfony doesn't know that my WidgetText is of type Widget too.
If in the controller (generated by Symfony) I change this line:
$this->createForm(new FormType(), new Form())
To:
$this->createForm(new FormType())
The form displays well but when submitting I've got no data binded.
I'm completly stuck there, from an OOP point of view I think this should work but I'm not sure if Symfony allows me to do what I want.
As mentioned in the comments of your question, you should change the name of the "WidgetText" field to "widgets". The reason behind that is that the names of the fields should match the accessors in your model (i.e. "name" for (set|get)Name(), "widgets" for (set|get)Widgets() etc.)
If you really want the name of a field to differ from the accessor in the model, you can also use the "property_path" option (which is set to the field's name by default):
$builder->add('WidgetText', ..., array(
...
'property_path' => 'widgets',
));

Catchable Fatal Error: Argument 1 passed to ? Symfony2

I am stuck and frustrated with the bellow error message:
Catchable Fatal Error: Argument 1 passed to Medicine\UserBundle\Entity\User
::setUsertype() must be an instance of Medicine\UserBundle\Entity\Usertype,
instance of Doctrine\Common\Collections\ArrayCollection given, called in
/opt/lampp/htdocs/drugs/vendor/symfony/src/Symfony/Component/Form/Util
/PropertyPath.php on line 347 and defined in /opt/lampp/htdocs/drugs/src/
Medicine/UserBundle/Entity/User.php line 224
What I think this error is due to use of manytoone field in my entity, I even tried with keeping onetomany in another entity.
I have a user entity and a usertype entity, the usertype_id is a manytoone field in user table. here is the code for both the entities:-
User
namespace Medicine\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="Medicine\UserBundle\Repository\UserRepository")
* #ORM\Table(name="user")
* #ORM\HasLifecycleCallbacks()
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\HasLifecycleCallbacks()
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $username;
/**
* #ORM\ManyToOne(targetEntity="Usertype", inversedBy="users")
* #ORM\JoinColumn(name="usertype_id", referencedColumnName="id")
*/
protected $usertype;
/**
* #ORM\Column(type="string")
*/
protected $image;
/**
* Set usertype
*
* #param Medicine\UserBundle\Entity\Usertype $usertype
*/
public function setUsertype(\Medicine\UserBundle\Entity\Usertype $usertype)
{
$this->usertype = $usertype;
}
/**
* Get usertype
*
* #return Medicine\UserBundle\Entity\Usertype
*/
public function getUsertype()
{
return $this->usertype;
}
}
I am just showing the concerned code, i have all the getter and setter methods for the above code.
UserType
namespace Medicine\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="Medicine\UserBundle\Repository\UsertypeRepository")
* #ORM\Table(name="usertype")
* #ORM\HasLifecycleCallbacks()
*/
class Usertype
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\HasLifecycleCallbacks()
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="usertype")
*/
protected $users;
public function __construct()
{
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add users
*
* #param Medicine\UserBundle\Entity\User $users
*/
public function addUser(\Medicine\UserBundle\Entity\User $users)
{
$this->users[] = $users;
}
/**
* Get users
*
* #return Doctrine\Common\Collections\Collection
*/
public function getUsers()
{
return $this->users;
}
}
Controller
This Executes when a user wants to login. He will fill in the username password and a UserType:
public function indexAction()
{
$entity = new User();
$form = $this->createForm(new LoginForm(), $entity);
$request = $this->getRequest();
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
echo "<pre>"; print_r($entity->getUsertype()); exit;
$em = $this->getDoctrine()
->getEntityManager();
$em->persist($entity);
$userrepository = $em->getRepository('MedicineUserBundle:User');
echo "<pre>"; print_r($entity->getUsertype()); exit;
$all = $userrepository->findOneBy(array('login' => $entity->getLogin(), 'password' => $entity->getPassword()));
if($all)
{
return $this->redirect($this->generateUrl('MedicineUserBundle_login'));
}
}
}
return $this->render('MedicineUserBundle:User:loginpage.html.twig',array(
'form' => $form->createView()
));
}
Login Form
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('login', 'text', array('label' => 'Username',))
->add('password','password')
->add('usertype', 'entity', array('class' => 'MedicineUserBundle:Usertype', 'property'=>'name', 'multiple' => true, ))
;
}
The 'multiple' => true in combination with your entity association definition is causing this problem.
You should find that if you change multiple to false (and as such can only select one UserType for your User), things work properly.
If you want multiple UserTypes for one User, you have a Many-to-Many association - one user can have many UserTypes, and one UserType can have many Users. See Doctrine's ManyToMany association type to implement this. Documentation Here.
Hope this helps.

Resources