alexcool68
alexcool68

Reputation: 64

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

Upvotes: 1

Views: 5382

Answers (1)

Mulcek
Mulcek

Reputation: 96

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.

Bonus update:

  1. form option 'placeholder' => null, takes care that no default 'empty' option is available.
  2. form option 'required' => true, forces html5 form popup validation.
  3. Then you should use something like entity @assert notations and use validator constraints on entity attribute:

    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @var string
     *
     * @Assert\NotNull()
     * @ORM\Column(name="modele", type="string", length=255, unique=true)
     */
    private $modele;
    
  4. You could also disallow edit form from opening via controller editAction (maybe some redirect) and twig, where you could hide edit button.

Upvotes: 2

Related Questions