alexcool68 alexcool68 - 2 months ago 8
PHP Question

Adding choices in entityType

I'm learning by myself the symfony framework (my job is not about developing, I'm not a developer) and I find out most of case the solution but here, is one what I didn't know how to manage.

I have 2 entity :

Product:

/**
* Product
*
* @ORM\Table(name="product")
* @ORM\Entity(repositoryClass="ProductBundle\Repository\ProductRepository")
* @UniqueEntity("productNumber")
*/
class Product
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @var string
*
* @ORM\Column(name="productNumber", type="string", length=255, unique=true)
* @Assert\Regex(
* pattern="/[0-9][.][0-9]{3}/",
* message="It should be like 1.234"
* )
*/
private $productNumber;

/**
* @ORM\ManyToOne(targetEntity="ProductGroup")
*/
private $productGroup;

/**
* Constructor
*/
public function __construct()
{

}
}


Camera :

/**
* Camera
*
* @ORM\Table(name="camera")
* @ORM\Entity(repositoryClass="ProductBundle\Repository\CameraRepository")
*/
class Camera
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @var string
*
* @ORM\Column(name="modele", type="string", length=255, unique=true)
*/
private $modele;

/**
* @var string
*
* @ORM\Column(name="description", type="text")
*/
private $description;

/**
*
* @ORM\ManyToOne(targetEntity="Product")
*/
private $product;

/**
* @ORM\ManyToMany(targetEntity="CustomField", inversedBy="camera", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $customFields;

/**
* Constructor
*/
public function __construct()
{
$this->customFields = new ArrayCollection();
}
}


My form :

namespace ProductBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;

class CameraType extends AbstractType {

/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {

$builder
->add('product', EntityType::class, [
'class' => 'ProductBundle:Product',

'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('p')
->select('p')
->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
->where('c.product IS NULL')
;
},
'attr' => [
'required' => true,
],

'choice_label' => 'productNumber',

])
->add('modele', TextType::class, [
'label' => "Modele",
])
->add('description', TextType::class, [
'label' => "Description",
])
->add('customFields', CollectionType::class, [
'entry_type' => CustomFieldType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'required' => false,
'attr' => [
'class' => 'customfield'
]
])
;
}

/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ProductBundle\Entity\Camera'
));
}

}


When I add a camera, I would like only the Product:productNumber where are available (not take by a camera), the querybuilder is working but my issue concern the edit form, it show only available productNumber so it's changing every time I need to edit this camera.

What can I handle this ? Should I try to found another way to add a productNumber ? do you have a "trick" ?

I hope you will understand the problem and my english because it's not my first language.

Have a nice day.

Edit : I'm on Symfony 3.1.4

Answer

I presume on new form your choice field shows only unused ProductBundle:Camera entity, and on edit form it should show saved ProductBundle:Camera entity and all unused ones.

You should look into Form Event Subscribers

You need to implement two event listeners PRE_SET_DATA and PRE_SUBMIT.

Here is one way to do it. Something like this works on SF 2.8 First you will have to create product entity form from custom ProductFieldSubscriber which becomes EventSubscriberInterface:

$builder->addEventSubscriber(new ProductFieldSubscriber('product', [])

Now ProductFieldSubscriber should look something like this (untested)

namespace ProductBundle\Form\EventListener;

use Symfony\Component\Form\FormInterface,
    Symfony\Component\Form\FormEvent,
    Symfony\Component\EventDispatcher\EventSubscriberInterface,
    Symfony\Component\Form\FormEvents,
    Doctrine\ORM\EntityRepository,
    Symfony\Bridge\Doctrine\Form\Type as DoctrineTypes
;


class ProductFieldSubscriber implements EventSubscriberInterface
{
    private $propertyPathToSelf;

    public function __construct($propertyPathToSelf, array $formOptions=[]) {
        $this->propertyPathToSelf = $propertyPathToSelf;
        $this->formOptions = $formOptions;
    }

    public static function getSubscribedEvents() {
        return [
            FormEvents::PRE_SET_DATA => 'onPreSetData',
            FormEvents::PRE_SUBMIT => 'onPreSubmit',
        ];
    }

    private function addForm(FormInterface $form, $selfId = null) {

        $formOptions = array_replace_recursive ([
                    'class'         => 'ProductBundle:Product',
                    'placeholder'   => null,
                    'compound'      => false,
                    'query_builder' => function (EntityRepository $er) use ($selfId) {
                        $qb = $er->createQueryBuilder('p')
                            ->select('p')
                            ->leftJoin('ProductBundle:Camera', 'c', 'WITH', 'c.product = p.id')
                            ->where('c.product IS NULL')
                        ;

                        if (null !== $selfId) {
                            $qb
                                ->orWhere($qb->expr()->eq('p.product', ':existingId'))
                                ->setParameter('existingId', $selfId->getId())
                            ;
                        }

                        return $qb;
                    },
                ],
                $this->formOptions
            );

        if ($selfId) {
            $formOptions['data'] = $selfId;
        }

        $form->add($this->propertyPathToSelf, DoctrineTypes\EntityType::class, $formOptions);

    }

    public function onPreSetData(FormEvent $event) {
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        $selfIdTypeMethod = "get{$this->propertyPathToSelf}";
        $selfId = $data->$selfIdTypeMethod();

        $this->addForm($form, $selfId);
    }

    public function onPreSubmit(FormEvent $event) {
        $data = $event->getData();
        $form = $event->getForm();

        $selfId = array_key_exists($this->propertyPathToSelf, $data) ? $data[$this->propertyPathToSelf] : null;

        $this->addForm($form, $selfId);
    }
}

Query builder would be simpler if you had mapped entity relations.

Comments