Many to Many with Join Table in EasyAdminBundle - symfony

I'm trying to get a smooth admin interface similar as if when two entities are associated by a many-to-many relationship. I need the join table to define additional information like rank. I don't want to display the jointable entity on the backend, it should be writeable in the edit-menu of at least one entity. My example:
class Question{
/**
* #Assert\NotBlank()
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Options2Questions", mappedBy="question", cascade={"persist"})
*/
private $optionQuestion;
public function __construct() {
$this->optionQuestion = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addOptionQuestion(\AppBundle\Entity\Options2Questions $optionQuestion){
$this->optionQuestion[] = $optionQuestion;
return $this;
}
public function removeOptionQuestion(\AppBundle\Entity\Options2Questions $optionQuestion){
$this->optionQuestion->removeElement($optionQuestion);
}
public function getOptionQuestion(){
return $this->optionQuestion;
}
}
class Options{
/**
* #Assert\NotBlank()
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Options2Questions", mappedBy="options")
*/
private $optionQuestion;
public function __construct(){
$this->optionQuestion = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addOptionQuestion(\AppBundle\Entity\Options2Questions $optionQuestion){
$this->optionQuestion[] = $optionQuestion;
return $this;
}
public function removeOptionQuestion(\AppBundle\Entity\Options2Questions $optionQuestion)
{
$this->optionQuestion->removeElement($optionQuestion);
}
public function getOptionQuestion(){
return $this->optionQuestion;
}
}
class Options2Questions
{
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Options", inversedBy="optionsQuestions")
* #ORM\JoinColumn(name="options_id", referencedColumnName="id", nullable=false)
*/
private $options;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Question", inversedBy="optionsQuestions")
* #ORM\JoinColumn(name="question_id", referencedColumnName="id", nullable=false)
*/
private $question;
/**
* #var int
*
* #ORM\Column(name="rank", type="integer", nullable=true)
*/
private $rank;
/**
* Set rank
*
* #param integer $rank
*
* #return Options2Questions
*/
public function setRank($rank)
{
$this->rank = $rank;
return $this;
}
/**
* Get rank
*
* #return int
*/
public function getRank()
{
return $this->rank;
}
/**
* Set options
*
* #param \AppBundle\Entity\Options $options
*
* #return Options2Questions
*/
public function setOptions(\AppBundle\Entity\Options $options)
{
$this->options = $options;
return $this;
}
/**
* Get options
*
* #return \AppBundle\Entity\Options
*/
public function getOptions()
{
return $this->options;
}
/**
* Set question
*
* #param \AppBundle\Entity\Question $question
*
* #return Options2Questions
*/
public function setQuestion(\AppBundle\Entity\Question $question)
{
$this->question = $question;
return $this;
}
/**
* Get question
*
* #return \AppBundle\Entity\Question
*/
public function getQuestion()
{
return $this->question;
}
}
I could not find any documentation on this. Where should I look specifically?

First of all you have to create the admin class - service for the all of these three entities. Therefore you will have the next 3 classes:
a) QuestionAdmin
b) OptionAdmin
c) Options2QuestionsAdmin
If you want to remove from you admin services list the Options2QuestionsAdmin - you can add show_in_dashboard: false line to the services.yml:
app.admin.options2questions:
class: AppBundle\Options2questionsAdmin
arguments: [~, AppBundle\EntityOptions2questions, SonataAdminBundle:CRUD]
tags:
- { name: sonata.admin, manager_type: orm, group: 'your_group', label: 'your_label', show_in_dashboard: false }
In your QuestionAdmin class in formmapper method you can add this:
class QuestionAdmin extends AbstractAdmin
{
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('optionQuestion', 'sonata_type_collection', [
'required' => true,
'label' => 'option_question',
'by_reference' => false,
'btn_add' => 'add_button',
'type_options' => [
'delete' => true,
],
], [
'edit' => 'inline',
'inline' => 'standard',
'sortable' => 'id',
'allow_delete' => true,
])
;
}

Related

Symfony 4 - Can't create Entity

I'm using Sonata Admin for the first time, I followed doc online and my admin works well, except I have an error when I try to create elements via my admin:
Failed to create object: App\Entity\HomeBlockElement
I have my HomeBlockElement
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\HomeBlockElementRepository")
* #ORM\Table(name="home_block_element")
*/
class HomeBlockElement
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="home_element_id",type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="home_element_title", type="string", length=255)
*/
protected $title;
/**
* #var string
*
* #ORM\Column(name="home_element_text_link", type="string", length=255)
*/
protected $textLink;
/**
* #var string
*
* #ORM\Column(name="home_element_shopping_link", type="string", length=255)
*/
protected $shoppingLink;
/**
* #var string
*
* #ORM\Column(name="home_element_marketing_etiquette", type="string", length=255)
*/
protected $marketingEtiquette;
/**
* #var string
*
* #ORM\Column(name="home_element_media", type="text")
*/
protected $media;
/**
* #var bool
*
* #ORM\Column(name="home_element_published", type="boolean")
*/
protected $published = false;
public function getId(): ?int
{
return $this->id;
}
/**
* #return string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* #param string $title
* #return HomeBlockElement
*/
public function setTitle(?string $title): HomeBlockElement
{
$this->title = $title;
return $this;
}
/**
* #return string
*/
public function getTextLink(): ?string
{
return $this->textLink;
}
/**
* #param string $textLink
* #return HomeBlockElement
*/
public function setTextLink(?string $textLink): HomeBlockElement
{
$this->textLink = $textLink;
return $this;
}
/**
* #return string
*/
public function getShoppingLink(): ?string
{
return $this->shoppingLink;
}
/**
* #param string $shoppingLink
* #return HomeBlockElement
*/
public function setShoppingLink(?string $shoppingLink): HomeBlockElement
{
$this->shoppingLink = $shoppingLink;
return $this;
}
/**
* #return string
*/
public function getMarketingEtiquette(): ?string
{
return $this->marketingEtiquette;
}
/**
* #param string $categoryLink
* #return HomeBlockElement
*/
public function setMarketingEtiquette(?string $marketingEtiquette): HomeBlockElement
{
$this->marketingEtiquette = $marketingEtiquette;
return $this;
}
/**
* #return bool
*/
public function isPublished(): bool
{
return $this->published;
}
/**
* #param bool $published
* #return Page
*/
public function setPublished(bool $published): HomeBlockElement
{
$this->published = $published;
return $this;
}
}
And my HomeBlockElementAdmin:
<?php
namespace App\Admin;
use App\Entity\Page;
use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Route\RouteCollection;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
final class HomeBlockElementAdmin extends AbstractAdmin
{
protected $datagridValues = array(
'_sort_order' => 'ASC',
'_sort_by' => 'title',
);
/**
* #param $object
* #return string|null
*/
public function toString($object): ?string
{
return $object instanceof Page && $object->getTitle()
? $object->getTitle()
: 'Nouveau bloc élément';
}
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('Contenu')
->add('published', CheckboxType::class, ['required' => false, 'label' => 'Publier'])
->add('title', TextType::class, ['required' => true, 'label' => 'Titre'])
->add('marketingEtiquette', TextType::class, ['required' => false, 'label' => 'Etiquette Marketing'])
->add('textLink', TextType::class, ['required' => true, 'label' => 'Texte'])
->add('shoppinglink', TextType::class, ['required' => true, 'label' => 'Lien'])
->end();
}
protected function configureListFields(ListMapper $listMapper)
{
unset($this->listModes['mosaic']);
$listMapper
->addIdentifier('title')
->addIdentifier('marketingEtiquette')
->addIdentifier('textLink')
->addIdentifier('shoppinglink')
->addIdentifier('published')
;
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
->add('marketingEtiquette')
->add('textLink')
->add('shoppinglink')
->add('published');
}
/**
* #param RouteCollection $collection
*/
protected function configureRoutes(RouteCollection $collection)
{
$collection
->remove('delete')
//->remove('create')
->add('move', $this->getRouterIdParameter() . '/move/{position}');
}
}
I then defined this in my services.yaml
admin.element:
class: App\Admin\HomeBlockElementAdmin
arguments: [~, App\Entity\HomeBlockElement, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: 'Element blocs', group: 'app.admin.group.home' }
I've updated the data base with php bin/console doctrine:schema:update to make it match with my code and everything worked fine.From what I saw on the Internet, the error is related to SQL but
I can't see where the error comes from.
In the breadrcumb from the error I can see:
PDOException > PDOException > NotNullConstraintViolationException > ModelManagerException
So the error is because something is null when it shouldn't? But the only thing that is required is the id, and it should be generated automatically... I don't know what to do

Sonata Admin One to Many relations

I started working with SonataAdmin Bundle today, and i can't figure out OneToMany relations. My User can follow(obserwowane) offerts
Entities:
class Obserwowane {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Oferty",inversedBy="obserwowane")
* #ORM\JoinColumn(name="oferta", referencedColumnName="id_oferty", onDelete="CASCADE")
*/
protected $oferta;
/**
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="obserwowane")
* #ORM\JoinColumn(name="user", referencedColumnName="id")
*/
protected $user;
}
.
class User {
/**
* #ORM\OneToMany(targetEntity="Obserwowane", mappedBy="user")
*/
public $obserwowane;
}
.
class Oferty {
/**
* #ORM\OneToMany(targetEntity="Obserwowane", mappedBy="oferta")
*/
protected $obserwowane;
}
My servies.yml -> http://pastebin.com/biNCLhNt
I would like to display followed offers in User form in SonataAdminBundle. I would like also to have it editable.
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('id', 'integer', array('label' => 'id'))
->add('username', 'text', array('label' => 'Username'))
->add('email', 'text', array('label' => 'e-mail'))
->add('password', 'text', array('label' => 'Password'))
;
}
Define obserwowane as ArrayCollection and add getter and setter methods so sonata will use them to operate with Obserwowane array of entities.
use Doctrine\Common\Collections\ArrayCollection;
class user{
// ...
public function __construct()
{
$this->obserwowane = new ArrayCollection;
}
/**
* Get obserwowane
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getObserwowane ()
{
return $this->obserwowane ;
}
public function setObserwowane (ArrayCollection $obserwowane )
{
$this->obserwowane = $obserwowane ;
return $this;
}
/**
* Add Obserwowane
*
* #param Obserwowane $obserwowane
* #return Obserwowane
*/
public function addObserwowane (Obserwowane $obserwowane )
{
$this->obserwowane[] = $obserwowane;
return $this;
}
/**
* Remove Obserwowane
*
* #param Obserwowane $obserwowane
*/
public function removeObserwowane(Obserwowane $obserwowane)
{
$this->obserwowane->removeElement($obserwowane);
}
}
Finally add obserwowane field in formMapper
$formMapper
->add('obserwowane')
Update
To add or remove user for Obserwowane entity add those functions to Obserwowaneclass
class Obserwowane{
// ..
/**
* Set User
*
* #param User $user
* #return User
*/
public function setUser($user)
{
$this->user = $user;
return $this;
}
/**
* Get User
*
* #return User
*/
public function getUser()
{
return $this->user;
}
}
And in sonata
$formMapper
->add(user)

Symfony: ManyToMany relationship between Sonata User and my bundle Entity

I'm trying to create a ManyToMany relationship between Sonata's User and an entity called "Network" that resides in my bundle.
The idea is to be able to assign multiple Networks to each user via the User creation/edition of SonataAdmin.
Here's what I've got:
User Entity:
<?php
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class User extends BaseUser
{
/**
* #var integer $id
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="\Acme\TestBundle\Entity\Network", mappedBy="users", cascade={"persist", "remove"})
*/
protected $networks;
public function __construct()
{
parent::__construct();
$this->networks = new ArrayCollection();
}
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Add network
*
* #param \Acme\TestBundle\Entity\Network $network
* #return User
*/
public function addNetwork($network)
{
$this->networks[] = $network;
$network->addUser($this);
return $this;
}
/**
* Remove network
*
* #param \Acme\TestBundle\Entity\Network $network
*/
public function removeNetwork($network)
{
$this->networks->removeElement($network);
$network->removeUser($this);
}
/**
* Get networks
*
* #return \Doctrine\Common\Collections\ArrayCollection
*/
public function getNetworks()
{
return $this->networks;
}
}
Network Entity:
<?php
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
*/
class Network
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=255)
*/
protected $name;
/**
* #ORM\ManyToMany(targetEntity="\Application\Sonata\UserBundle\Entity\User", inversedBy="networks")
* #ORM\JoinTable(name="Networks_Users")
*/
protected $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function __toString()
{
return $this->getName();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Network
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add user
*
* #param \Application\Sonata\UserBundle\Entity\User $user
* #return Network
*/
public function addUser($user)
{
$this->users[] = $user;
return $this;
}
/**
* Remove user
*
* #param \Application\Sonata\UserBundle\Entity\User $user
*/
public function removeUser($user)
{
$this->users->removeElement($user);
}
/**
* Get users
*
* #return \Doctrine\Common\Collections\ArrayCollection
*/
public function getUsers()
{
return $this->users;
}
}
app\config.yml
sonata_user:
admin:
user:
class: Acme\TestBundle\Admin\UserAdmin
#...#
User Admin class:
<?php
namespace Acme\TestBundle\Admin;
use Sonata\UserBundle\Admin\Model\UserAdmin as SonataUserAdmin;
class UserAdmin extends SonataUserAdmin
{
/**
* {#inheritdoc}
*/
protected function configureFormFields(\Sonata\AdminBundle\Form\FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
$formMapper
->with('Networks')
->add('networks', 'sonata_type_model', array(
'by_reference' => false,
'required' => false,
'expanded' => false,
'multiple' => true,
'label' => 'Choose the user Networks',
'class' => 'AcmeTestBundle:Network'
))
->end();
}
}
Here's the problem:
When I edit an existing User via SonataAdmin and assign certain Networks to it changes are persisted to the database but the selector appears empty, as if no Networks were ever assigned. If I try to assign Networks again, I get a database constraint violation message (as it would be expected).
I discovered that for some reason the $networks ArrayCollection is returning NULL, as if the relationship could not be established from the User side:
$usr = $this->getUser();
$selected_networks = $usr->getNetworks(); // returns NULL
If I attempt to manage Users from the Network side, everything works fine (with no additions or changes to the actual code):
<?php
namespace Acme\TestBundle\Admin;
use Sonata\AdminBundle\Form\FormMapper;
class NetworkAdmin extends Admin
{
/**
* #param \Sonata\AdminBundle\Form\FormMapper $formMapper
*
* #return void
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('name')
->end()
->with('Users')
->add('users', 'sonata_type_model', array(
'by_reference' => false,
'required' => false,
'expanded' => false,
'multiple' => true,
'label' => 'Choose the network Users',
'class' => 'ApplicationSonataUserBundle:User'
))
;
}
}
I'm stumped. Any ideas?
Thank you all.

Submitting form data to entity1 and entity2_id column is null even when entity1 and entity2 are mapped

I have two entities (Plan and PricingTier) that are set up to be mapped to each other. The PricingTier is set up to be a oneToMany and the Plan is set up to be a manyToOne. The mapped column is in the Plan entity; the plans database table has a pricing_tier_id column that links the two tables/entities together.
I have a form that creates a new plan. The form generates properly in the Twig file and when posted the $request->request->getAll(); returns an array of the posted values. In the array I can see that the pricingTierId has clearly been set to the id of the pricing tier I selected. When I peform the following:
$form->bind($request);
$newPlan = $form->getData();
$em = $this->getDoctrine()->getEntityManager();
$em->perist($newPlan);
$em->flush();
I get a thrown exception saying that pricing_tier_id can not be NULL. I have done a var_dump() to the $newPlan variable and it looks returns an object, including the object of the mapped pricing tier.
Can anyone suggest a solution to why I'd be getting this error? Relevant code and errors are below.
PlanController.php
namespace etrak\CustomerServiceBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use etrak\OnlineOrderProcessingBundle\Entity\Plan;
use etrak\OnlineOrderProcessingBundle\Entity\PricingTier;
use etrak\CustomerServiceBundle\Form\Type\PlanType;
use Symfony\Component\HttpFoundation\Request;
class PlanController extends Controller
{
public function indexAction($name)
{
return $this->render('etrakCustomerServiceBundle:Default:index.html.twig', array('name' => $name));
}
public function addPlanAction(Request $request)
{
// Set up a new Plan object
$plan = new Plan();
$form = $this->createForm(new PlanType(), $plan);
// Check to see if the form has been submitted
if ($request->isMethod('POST')) {
$form->bind($request);
var_dump($request->request->all()); die();
// Validate the form
if ($form->isValid()) {
$newPlan = $form->getData();
//var_dump($newPlan->getPricingTierId()); die();
$em = $this->getDoctrine()->getEntityManager();
$em->persist($newPlan);
$em->flush();
}
}
return $this->render('etrakCustomerServiceBundle:Plan:new.html.twig', array("form" => $form->createView()));
}
}
PlanType.php
namespace etrak\CustomerServiceBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PlanType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'etrak\OnlineOrderProcessingBundle\Entity\Plan',
'cascade_validation' => true,
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$termsConditionsArray = array("1 Year Contract" => "1 Year Contract", "2 Year Contract" => "2 Year Contract");
$billingFrequencyArray = array("1" => "Monthly", "6" => "6 Months", "12" => "Yearly");
// Create the form
$builder->add('name', 'text', array('label' => 'Plan Name: ', 'required' => false));
$builder->add('description', 'text', array('label' => 'Plan Description: '));
$builder->add('termsConditions', 'choice', array('choices' => $termsConditionsArray, 'label' => 'Terms & Conditions'));
$builder->add('amount', 'text', array('label' => 'Plan Price: '));
$builder->add('affinity', 'choice', array('choices' => array('0' => 'Yes', '1' => 'No'), 'label' => 'Affinity? ', 'expanded' => true));
$builder->add('deactivationFee', 'text', array('label' => "Deactivation Fee: "));
$builder->add('recurringInMonths', 'choice', array('choices' => $billingFrequencyArray, 'label' => 'Billing Frequency: '));
$builder->add('pricingTierId', 'entity', array(
'class' => 'etrakOnlineOrderProcessingBundle:pricingTier',
'property' => 'name',
'label' => "Select Pricing Tier: "
));
$builder->add('activeStartDate', 'datetime', array('label' => "Effective Start Date: "));
$builder->add('activeEndDate', 'datetime', array('label' => "Effective End Date: "));
}
public function getName()
{
return 'plan';
}
}
Plan.php
namespace etrak\OnlineOrderProcessingBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Plan
*/
class Plan
{
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $name;
/**
* #var string
*/
private $description;
/**
* #var string
*/
private $termsConditions;
/**
* #var boolean
*/
private $active;
/**
* #var decimal
*/
private $amount;
/**
* #var boolean
*/
private $affinity;
/**
* #var integer
*/
private $deactivationFee;
/**
* #var integer
*/
private $gracePeriodDays;
/**
* #var integer
*/
private $recurringInMonths;
/**
* #var integer
*/
private $pricingTierId;
/**
* #var date
*/
private $activeStartDate;
/**
* #var date
*/
private $activeEndDate;
/**
* #var \etrak\OnlineOrderProcessingBundle\Entity\PricingTier
*/
private $pricingTier;
/**
* Set pricingTier
*
* #param \etrak\OnlineOrderProcessingBundle\Entity\PricingTier $pricingTier
* #return Plan
*/
public function setPricingTier(\etrak\OnlineOrderProcessingBundle\Entity\PricingTier $pricingTier = null)
{
$this->pricingTier = $pricingTier;
return $this;
}
/**
* Get pricingTier
*
* #return \etrak\OnlineOrderProcessingBundle\Entity\PricingTier
*/
public function getPricingTier()
{
return $this->pricingTier;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Plan
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Plan
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set termsConditions
*
* #param string $termsConditions
* #return Plan
*/
public function setTermsConditions($termsConditions)
{
$this->termsConditions = $termsConditions;
return $this;
}
/**
* Get termsConditions
*
* #return string
*/
public function getTermsConditions()
{
return $this->termsConditions;
}
/**
* Set active
*
* #param boolean $active
* #return Plan
*/
public function setActive($active)
{
$this->active = $active;
return $this;
}
/**
* Get active
*
* #return boolean
*/
public function getActive()
{
return $this->active;
}
/**
* Set amount
*
* #param decimal $amount
* #return Plan
*/
public function setAmount($amount)
{
$this->amount = $amount;
return $this;
}
/**
* Get amount
*
* #return decimal
*/
public function getAmount()
{
return $this->amount;
}
/**
* Set affinity
*
* #param boolean $affinity
* #return Plan
*/
public function setAffinity($affinity)
{
$this->affinity = $affinity;
return $this;
}
/**
* Get affinity
*
* #return boolean
*/
public function getAffinity()
{
return $this->affinity;
}
/**
* Set deactivationFee
*
* #param integer $deactivationFee
* #return Plan
*/
public function setDeactivationFee($deactivationFee)
{
$this->deactivationFee = $deactivationFee;
return $this;
}
/**
* Get deactivationFee
*
* #return integer
*/
public function getDeactivationFee()
{
return $this->deactivationFee;
}
/**
* Set gracePeriodDays
*
* #param integer $gracePeriodDays
* #return Plan
*/
public function setGracePeriodDays($gracePeriodDays)
{
$this->gracePeriodDays = $gracePeriodDays;
return $this;
}
/**
* Get gracePeriodDays
*
* #return integer
*/
public function getGracePeriodDays()
{
return $this->gracePeriodDays;
}
/**
* Set recurringInMonths
*
* #param integer $recurringInMonths
* #return Plan
*/
public function setRecurringInMonths($recurringInMonths)
{
$this->recurringInMonths = $recurringInMonths;
return $this;
}
/**
* Get recurringInMonths
*
* #return integer
*/
public function getRecurringInMonths()
{
return $this->recurringInMonths;
}
/**
* Set pricingTierId
*
* #param integer $pricingTierId
* #return Plan
*/
public function setPricingTierId($pricingTierId)
{
$this->pricingTierId = $pricingTierId;
return $this;
}
/**
* Get pricingTierId
*
* #return integer
*/
public function getPricingTierId()
{
return $this->pricingTierId;
}
/**
* Set activeStartDate
*
* #param \DateTime $activeStartDate
* #return Plan
*/
public function setActiveStartDate($activeStartDate)
{
$this->activeStartDate = $activeStartDate;
return $this;
}
/**
* Get activeStartDate
*
* #return \DateTime
*/
public function getActiveStartDate()
{
return $this->activeStartDate;
}
/**
* Set activeEndDate
*
* #param \DateTime $activeEndDate
* #return Plan
*/
public function setActiveEndDate($activeEndDate)
{
$this->activeEndDate = $activeEndDate;
return $this;
}
/**
* Get activeEndDate
*
* #return \DateTime
*/
public function getActiveEndDate()
{
return $this->activeEndDate;
}
/**
*
*/
public function prePersist()
{
if (!isset($this->affinity)) {
$this->setAffinity(0);
}
if (!isset($this->active)) {
$this->setActive(1);
}
}
}
Plan.orm.yml
#etrak/OnlineOrderProcessingBundle/Resources/config/doctrine/Entity/Plan.orm.yml
etrak\OnlineOrderProcessingBundle\Entity\Plan:
type: entity
table: plans
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 255
nullable: true
description:
type: text
nullable: true
termsConditions:
column: terms_conditions
type: text
nullable: true
active:
type: boolean
nullable: true
amount:
type: decimal
nullable: true
scale: 2
precision: 5
affinity:
type: boolean
nullable: true
deactivationFee:
column: deactivation_fee
type: decimal
scale: 2
precision: 5
nullable: true
gracePeriodDays:
column: grace_period_days
type: integer
nullable: true
recurringInMonths:
column: recurring_in_months
type: integer
nullable: true
pricingTierId:
column: pricing_tier_id
type: integer
activeStartDate:
column: active_start_date
type: date
activeEndDate:
column: active_end_date
type: date
lifecycleCallbacks:
prePersist: [ prePersist ]
manyToOne:
pricingTier:
targetEntity: PricingTier
inversedBy: plans
joinColumn:
name: pricing_tier_id
referencedColumnName: id
PricingTier.php
namespace etrak\OnlineOrderProcessingBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* PricingTier
*/
class PricingTier
{
/**
* #var integer
*/
private $id;
/**
* #var integer
*/
private $productId;
/**
* #var string
*/
private $name;
/**
* #var string
*/
private $description;
/**
* #var integer
*/
private $minimumDevices;
/**
* #var boolean
*/
private $isAffinity;
/**
* #var string
*/
private $keyname;
/**
* #var string
*/
private $createdBy;
/**
* #var datetime
*/
private $createdOn;
/**
* #var datetime
*/
private $updatedOn;
/**
* Set productId
*
* #param integer $productId
* #return PricingTier
*/
public function setProductId($productId)
{
$this->productId = $productId;
return $this;
}
/**
* Get productId
*
* #return integer
*/
public function getProductId()
{
return $this->productId;
}
/**
* #var \Doctrine\Common\Collections\Collection
*/
private $plans;
/**
* Constructor
*/
public function __construct()
{
$this->plans = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add plans
*
* #param \etrak\OnlineOrderProcessingBundle\Entity\Plan $plans
* #return PricingTier
*/
public function addPlan(\etrak\OnlineOrderProcessingBundle\Entity\Plan $plans)
{
$this->plans[] = $plans;
return $this;
}
/**
* Remove plans
*
* #param \etrak\OnlineOrderProcessingBundle\Entity\Plan $plans
*/
public function removePlan(\etrak\OnlineOrderProcessingBundle\Entity\Plan $plans)
{
$this->plans->removeElement($plans);
}
/**
* Get plans
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPlans()
{
return $this->plans;
}
/**
* Set name
*
* #param string $name
* #return PricingTier
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return PricingTier
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set minimumDevices
*
* #param integer $minimumDevices
* #return PricingTier
*/
public function setMinimumDevices($minimumDevices)
{
$this->minimumDevices = $minimumDevices;
return $this;
}
/**
* Get minimumDevices
*
* #return integer
*/
public function getMinimumDevices()
{
return $this->minimumDevices;
}
/**
* Set isAffinity
*
* #param boolean $isAffinity
* #return PricingTier
*/
public function setIsAffinity($isAffinity)
{
$this->isAffinity = $isAffinity;
return $this;
}
/**
* Get isAffinity
*
* #return boolean
*/
public function getIsAffinity()
{
return $this->isAffinity;
}
/**
* Set keyname
*
* #param string $keyname
* #return PricingTier
*/
public function setKeyname($keyname)
{
$this->keyname = $keyname;
return $this;
}
/**
* Get keyname
*
* #return string
*/
public function getKeyname()
{
return $this->keyname;
}
/**
* Set createdBy
*
* #param string $createdBy
* #return PricingTier
*/
public function setCreatedBy($createdBy)
{
$this->createdBy = $createdBy;
return $this;
}
/**
* Get createdBy
*
* #return string
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* Set createdOn
*
* #param \DateTime $createdOn
* #return PricingTier
*/
public function setCreatedOn($createdOn)
{
$this->createdOn = $createdOn;
return $this;
}
/**
* Get createdOn
*
* #return \DateTime
*/
public function getCreatedOn()
{
return $this->createdOn;
}
/**
* Set updatedOn
*
* #param \DateTime $updatedOn
* #return PricingTier
*/
public function setUpdatedOn($updatedOn)
{
$this->updatedOn = $updatedOn;
return $this;
}
/**
* Get updatedOn
*
* #return \DateTime
*/
public function getUpdatedOn()
{
return $this->updatedOn;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #ORM\PrePersist
*/
public function onPrePersist()
{
if (!isset($this->isAffinity)) {
$this->setIsAffinity(0);
}
$this->setCreatedOn(new \DateTime());
}
/**
* #ORM\PreUpdate
*/
public function onPreUpdate()
{
$this->setUpdatedOn(new \DateTime());
}
}
PricingTier.orm.yml
#etrak/OnlineOrderProcessingBundle/Resources/config/doctrine/Entity/PricingTier.orm.yml
etrak\OnlineOrderProcessingBundle\Entity\PricingTier:
type: entity
table: pricing_tiers
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
productId:
column: product_id
type: integer
name:
type: string
length: 50
description:
type: string
length: 100
nullable: true
minimumDevices:
column: minimum_devices
type: integer
isAffinity:
column: is_affinity
type: boolean
keyname:
type: string
length: 55
createdBy:
column: created_by
type: string
length: 20
createdOn:
column: created_on
type: datetime
updatedOn:
column: updated_on
type: datetime
nullable: true
lifecycleCallbacks:
prePersist: [ onPrePersist ]
preUpdate: [ onPreUpdate ]
oneToMany:
plans:
targetEntity: Plan
mappedBy: pricingTier
That should be all of the files that relate to this. I didn't include the Twig file because it is simply one line that renders the form that is generated by the createView() magic method in the PlanController.
Thanks in advanced!
In your Plan.orm.yml:
pricingTierId:
column: pricing_tier_id
type: integer
which is the same column name as your many-to-one join. This is probably a bad practice. This is not set to nullable: true, and is probably the source of your problem. Explicitly, you don't need this field. Also in your form, you are loading the entity class for the pricingTierId, which is not an entity, and I think symfony is quite confused by this.
$builder->add('pricingTierId', 'entity', array( // add pricingTier
'class' => 'etrakOnlineOrderProcessingBundle:pricingTier', // not pricingTierId
'property' => 'name',
'label' => "Select Pricing Tier: "
));
I expect if you delete the offending yaml portion and adjust the form type your problem will go away.

Symfony 2 - Multiple form fields for one database row

I'm using Symfony 2.1 and Doctrine 2.
I'm dealing with 2 main entities : Place and Feature, with a ManyToMany relationship between them.
There's many features in the database, and to group them by theme the Feature is also related to a FeatureCategory entity with a ManyToOne relationship.
Here's the code of the different entities :
The Place entity
namespace Mv\PlaceBundle\Entity;
…
/**
* Mv\PlaceBundle\Entity\Place
*
* #ORM\Table(name="place")
* #ORM\Entity(repositoryClass="Mv\PlaceBundle\Entity\Repository\PlaceRepository")
* #ORM\HasLifecycleCallbacks
*/
class Place
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
* #Assert\NotBlank
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="\Mv\MainBundle\Entity\Feature")
* #ORM\JoinTable(name="places_features",
* joinColumns={#ORM\JoinColumn(name="place_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="feature_id", referencedColumnName="id")}
* )
*/
private $features;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Place
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add features
*
* #param \Mv\MainBundle\Entity\Feature $features
* #return Place
*/
public function addFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features[] = $features;
echo 'Add "'.$features.'" - Total '.count($this->features).'<br />';
return $this;
}
/**
* Remove features
*
* #param \Mv\MainBundle\Entity\Feature $features
*/
public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features->removeElement($features);
}
/**
* Get features
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFeatures()
{
return $this->features;
}
public function __construct()
{
$this->features = new \Doctrine\Common\Collections\ArrayCollection();
}
The Feature Entity :
namespace Mv\MainBundle\Entity;
…
/**
* #ORM\Entity
* #ORM\Table(name="feature")
* #ORM\HasLifecycleCallbacks
*/
class Feature
{
use KrToolsTraits\PictureTrait;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="label", type="string", length=255)
* #Assert\NotBlank()
*/
protected $label;
/**
* #ORM\ManyToOne(targetEntity="\Mv\MainBundle\Entity\FeatureCategory", inversedBy="features", cascade={"persist"})
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set label
*
* #param string $label
* #return Feature
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* Get label
*
* #return string
*/
public function getLabel()
{
return $this->label;
}
/**
* Set category
*
* #param Mv\MainBundle\Entity\FeatureCategory $category
* #return Feature
*/
public function setCategory(\Mv\MainBundle\Entity\FeatureCategory $category = null)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* #return Mv\MainBundle\Entity\FeatureCategory
*/
public function getCategory()
{
return $this->category;
}
}
The FeatureCategory entity :
namespace Mv\MainBundle\Entity;
...
/**
* #ORM\Entity
* #ORM\Table(name="feature_category")
*/
class FeatureCategory
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="code", type="string", length=255)
* #Assert\NotBlank()
*/
protected $code;
/**
* #ORM\Column(name="label", type="string", length=255)
* #Assert\NotBlank()
*/
protected $label;
/**
* #ORM\OneToMany(targetEntity="\Mv\MainBundle\Entity\Feature", mappedBy="category", cascade={"persist", "remove"}, orphanRemoval=true)
* #Assert\Valid()
*/
private $features;
public function __construct()
{
$this->features = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set code
*
* #param string $code
* #return Feature
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Get code
*
* #return string
*/
public function getCode()
{
return $this->code;
}
/**
* Set label
*
* #param string $label
* #return Feature
*/
public function setLabel($label)
{
$this->label = $label;
return $this;
}
/**
* Get label
*
* #return string
*/
public function getLabel()
{
return $this->label;
}
/**
* Add features
*
* #param \Mv\MainBundle\Entity\Feature $features
*/
public function addFeatures(\Mv\MainBundle\Entity\Feature $features){
$features->setCategory($this);
$this->features[] = $features;
}
/**
* Get features
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFeatures()
{
return $this->features;
}
/*
* Set features
*/
public function setFeatures(\Doctrine\Common\Collections\Collection $features)
{
foreach ($features as $feature)
{
$feature->setCategory($this);
}
$this->features = $features;
}
/**
* Remove features
*
* #param Mv\MainBundle\Entity\Feature $features
*/
public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
{
$this->features->removeElement($features);
}
/**
* Add features
*
* #param Mv\MainBundle\Entity\Feature $features
* #return FeatureCategory
*/
public function addFeature(\Mv\MainBundle\Entity\Feature $features)
{
$features->setCategory($this);
$this->features[] = $features;
}
}
Feature table is already populated, and users won't be able to add features but only to select them in a form collection to link them to the Place.
(The Feature entity is for the moment only linked to Places but will be later related to others entities from my application, and will contain all the features available for all entities)
In the Place form I need to display checkboxes of the features available for a Place, but I need to display them grouped by category.
Example :
Visits (FeatureCategory - code VIS) :
Free (Feature)
Paying (Feature)
Languages spoken (FeatureCategory - code LAN) :
English (Feature)
French (Feature)
Spanish (Feature)
My idea
Use virtual forms in my PlaceType form, like this :
$builder
->add('name')
->add('visit', new FeatureType('VIS'), array(
'data_class' => 'Mv\PlaceBundle\Entity\Place'
))
->add('language', new FeatureType('LAN'), array(
'data_class' => 'Mv\PlaceBundle\Entity\Place'
));
And create a FeatureType virtual form, like this :
class FeatureType extends AbstractType
{
protected $codeCat;
public function __construct($codeCat)
{
$this->codeCat = $codeCat;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('features', 'entity', array(
'class' => 'MvMainBundle:Feature',
'query_builder' => function(EntityRepository $er)
{
return $er->createQueryBuilder('f')
->leftJoin('f.category', 'c')
->andWhere('c.code = :codeCat')
->setParameter('codeCat', $this->codeCat)
->orderBy('f.position', 'ASC');
},
'expanded' => true,
'multiple' => true
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'virtual' => true
));
}
public function getName()
{
return 'features';
}
}
With this solution I get what I want but the bind process doesn't persist all the features. Instead of grouping them, it only keeps me and persist the last group "language", and erases all the previouses features datas. To see it in action, if I check the 5 checkboxes, it gets well into the Place->addFeature() function 5 times, but the length of the features arrayCollection is successively : 1, 2, 1, 2, 3.
Any idea on how to do it another way ? If I need to change the model I'm still able to do it.
What is the best way, reusable on my future other entities also related to Feature, to handle this ?
Thank you guys.
I think your original need is only about templating.
So you should not tweak the form and entity persistence logic to get the desired autogenerated form.
You should go back to a basic form
$builder
->add('name')
->add('features', 'entity', array(
'class' => 'MvMainBundle:Feature',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('f')
//order by category.xxx, f.position
},
'expanded' => true,
'multiple' => true
));
And tweak your form.html.twig

Resources