I had a similar problem. I needed to create a sale entity that needed to be associated in a many to one relationship with an enterprise entity and a many to many relationship with services entities. Here is the Sale Entity:
The thing is that services where available depending on the companies chosen. For instance services a and b could only be provided to company x. And services b and c could only be provided to company y. So in my admin, depending on the chosen company I had to display the available services. For these I needed to do 2 things:
First create a dynamic form with my sale admin, so that on the server side I could get the right services available for the company specified in my sale record. And second, I had to create a custom form type for my company form element, so that when it was changed by the user on the client side, It would send an ajax request to get the right services for the company chosen.
For my first problem, I did something similar to what you were trying to achieve, but instead of creating an specific custom type for my services element, I added de event listener directly in the admin.
Here is the Sale entity:
/**
*
* @ORM\Table(name="sales")
* @ORM\Entity
* @ORM\HasLifecycleCallbacks()
*/
class Sale
{
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="Branch")
* @ORM\JoinColumn(name="branch_id", referencedColumnName="id", nullable = false)
* @Assert\NotBlank(message = "Debe especificar una empresa a la cual asignar el precio de este exámen!")
*/
private $branch;
/** Unidirectional many to many
* @ORM\ManyToMany(targetEntity="Service")
* @ORM\JoinTable(name="sales_services",
* joinColumns={@ORM\JoinColumn(name="sale_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="service_id", referencedColumnName="id")}
* )
* @Assert\Count(min = "1", minMessage = "Debe especificar al menos un servicio a realizar!")
*/
private $services;
public function __construct() {
$this->services = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set branch
*
* @param Astricom\NeurocienciasBundle\Entity\Branch $branch
*/
//default value always have to be null, because when validation constraint is set to notblank,
//if default is not null, before calling the validation constraint an error will pop up explaining
//that no instance of Branch was passed to the $branch argument.
public function setBranch(\Astricom\NeurocienciasBundle\Entity\Branch $branch = null)
{
$this->branch = $branch;
}
/**
* Get branch
*
* @return Astricom\NeurocienciasBundle\Entity\Branch
*/
public function getBranch()
{
return $this->branch;
}
/**
* Add service
*
* @param \Astricom\NeurocienciasBundle\Entity\Service|null $service
*/
public function addServices(\Astricom\NeurocienciasBundle\Entity\Service $service = null)
{
$this->services[] = $service;
}
/**
* Get services
*
* @return Doctrine\Common\Collections\Collection
*/
public function getServices()
{
return $this->services;
}
/**
* Sets the creation date
*
* @param \DateTime|null $createdAt
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
}
/**
* Returns the creation date
*
* @return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Sets the last update date
*
* @param \DateTime|null $updatedAt
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
}
So then in the Admin form builder:
protected function configureFormFields(FormMapper $formMapper) {
$em = $this->container->get('doctrine')->getEntityManager();
$branchQuery = $em->createQueryBuilder();
$branchQuery->add('select', 'b')
->add('from', 'Astricom\NeurocienciasBundle\Entity\Branch b')
->add('orderBy', 'b.name ASC');
$formMapper
->with('Empresa/Sucursal')
->add('branch','shtumi_ajax_entity_type',array('required' => true, 'label'=>'Empresa/Sucursal','error_bubbling' => true, 'empty_value' => 'Seleccione una empresa/sucursal', 'empty_data' => null, 'entity_alias'=>'sale_branch', 'attr'=>array('add_new'=>false), 'model_manager' => $this->getModelManager(), 'class'=>'Astricom\NeurocienciasBundle\Entity\Branch', 'query' => $branchQuery))
->end()
;
$builder = $formMapper->getFormBuilder();
$factory = $builder->getFormFactory();
$sale = $this->getSubject();
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event) use ($sale,$factory, $em) {
$form = $event->getForm();
$servicesQuery = $em->createQueryBuilder();
$servicesQuery->add('select','s')
->add('from','Astricom\NeurocienciasBundle\Entity\Service s');
if (!$sale || !$sale->getId()) {
$servicesQuery
->where($servicesQuery->expr()->eq('s.id', ':id'))
->setParameter('id', 0);
}
else {
$servicesQuery
->join('s.branch', 'b')
->where($servicesQuery->expr()->eq('b.id', ':id'))
->setParameter('id', $sale->getBranch()->getId());
}
$form->add($factory->createNamed('services','entity',null,array('required' => true, 'label'=>'Servicios','error_bubbling' => true, 'attr'=>array('show_value_label'=>true),'class'=>'Astricom\NeurocienciasBundle\Entity\Service','multiple'=>true,'expanded'=>true,'query_builder'=>$servicesQuery)));
}
);
}
The trick thing was to pass the forms data. It doesn't work to use evet->getData() in the event listener's function. Instead I passed it through the admin->getSubject() method. Then instead of adding a sonataadmin form type, inside the event listener's function, I had to use a plain symfony form type.
The Ajax part as you mentioned is another question. All the weird things on the branch add method in the form builder is related to a customized field type for this matter. Don't worry about it.