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)
Related
I have a problem since 2 days! I try to upload file with vichuploaderBundle on a symfony 3.4 project.
I've already done this many times. But this time...It doesn't work and i don't understand why. On my local version, it work fine but on my production server it doesn't work.
Here is the error message:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'image_name' cannot be null
The file entity is persisted (with an id and a created date but the image name is empty???)it's like the vichuploader mapping doesn't work???
I have an Entity (NoteFrais) and each NoteFrais has a one relation with an another Entity (JustificatifDefraiement).
here is my JustificatifDefraiement entity:
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* JustificatifDefraiement
*
* #ORM\Table(name="justificatif_defraiement")
* #ORM\Entity(repositoryClass="MKG\MystiBundle\Repository \JustificatifDefraiementRepository")
* #vich\Uploadable
*/
class JustificatifDefraiement
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="justificatif", fileNameProperty="imageName")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* Constructor
*/
public function __construct()
{
$this->updatedAt = new \DateTime();
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|UploadedFile $justificatifDefraiement
* #return JustificatifDefraiement
*/
public function setImageFile(File $justificatifDefraiement = null)
{
$this->imageFile = $justificatifDefraiement;
if ($justificatifDefraiement) {
$this->updatedAt = new \DateTime();
}
return $this;
}
/**
* #return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
*
* #param $imageName
*
* #return $this
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
return $this;
}
/**
* #return string|null
*/
public function getImageName()
{
return $this->imageName;
}
/**
* Set updatedAt
*
* #param \DateTime $updatedAt
*
* #return JustificatifDefraiement
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
}
My form:
class JustificatifDefraiementType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('imageFile', FileType::class, array(
//'data_class' => null,
'label' => false,
'required' => true,
'attr' => array(
'class' => 'NoteFraisBootstrapFileInput',
'type' => 'file',
'placeholder' => 'Selectionner un justificatif (jpeg, png, jpg, pdf)',
'data-preview-file-type' => 'text',
'data-allowed-file-extensions' => '["jpeg", "png", "jpg", "pdf"]',
)
));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MKG\MystiBundle\Entity\JustificatifDefraiement'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'mkg_mystibundle_justificatifDefraiement';
}
}
The configuration:
parameters:
locale: fr
app.path.logos: /uploads/logos
app.path.imports: /uploads/imports
app.path.justificatifs: /uploads/justificatifs
I have this relation with another entity:
class NoteFrais
{
//.......//
/**
* #ORM\OneToOne(targetEntity="MKG\MystiBundle\Entity\JustificatifDefraiement", cascade={"persist"})
* #ORM\JoinColumn(name="justificatif_defraiement_id", referencedColumnName="id", onDelete="CASCADE", nullable=true)
*/
private $justificatifDefraiement;
//.......//
}
And the noteFraisType:
class NoteFraisType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//.......//
->add('justificatifDefraiement', JustificatifDefraiementType::class, array(
'required' => false));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MKG\MystiBundle\Entity\NoteFrais'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'mkg_mystibundle_notefrais';
}
}
Please help me!!
When you use VichUploader then you must use VichFileType inside your FormType
Then your buildForm ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('imageFile', VichFileType::class, array(
//Options ...
));
}
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,
])
;
}
I want to create table Pracownik which can contain zero or few instances of Zaklady and control it using Sonata Admin. Both of tables are translated using a2lix_translatable.
So I have classes:
<?php
namespace JCuryllo\InstituteBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* Pracownik
*
* #ORM\Table()
* #ORM\Entity
*/
class Pracownik
{
use ORMBehaviors\Translatable\Translatable;
public function __call($method, $arguments)
{
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
/**
* #ORM\ManyToOne(targetEntity="ZakladyTranslation")
**/
private $zaklady;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* and other properties
*/
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/* and other getters and setter */
}
And Translation:
<?php
namespace JCuryllo\InstituteBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* #ORM\Entity
*/
class PracownikTranslation
{
use ORMBehaviors\Translatable\Translation;
public function __toString()
{
return $this->getTitle();
}
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/* and other properties */
/**
* Set name
*
* #param string $name
* #return Pracownik
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/* and other getters and setter *.
}
I've used very similar code in Zaklady and ZakladyTranslation. Then in PracownikAdmin I try to do sth like:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('zaklady', 'many_to_one',array(
'required' => false,
))
->add('translations', 'a2lix_translations')
;
}
but it doesn't work (error: Could not load type "many_to_one").
many_to_one is not an available form type for Sonata, you have to use sonata_type_model or sonata_type_model_list for a many to one relation.
Example:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('zaklady', 'sonata_type_model_list',array(
'required' => false,
))
->add('translations', 'a2lix_translations');
}
Documentation : http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#advanced-usage-many-to-one
Ok, I have done it.
In Zaklady I added:
public function __toString()
{
return $this->getTitle(); // getTitle is in ZakladyTranslation!
}
In Pracownik I changed:
/**
* #ORM\ManyToOne(targetEntity="ZakladyTranslation")
**/
private $zaklady;
to:
/**
* #ORM\ManyToOne(targetEntity="Zaklady")
**/
private $zaklady;
and PracownikAdmin:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('translations', 'a2lix_translations')
->add('zaklady', 'sonata_type_model', array(
'multiple' => true,
'by_reference' => false,
'required' => false
))
;
}
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.
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