Symfony4/5 forms Cheatsheet

FormType / Configuration

  • // /src/Form/Type/FooFormType.php
    
    namespace App\Form\Type;
    
    class fooFormType extends AbstractType
    {
        // Inject a service
        protected $myService;
        public function __construct(MyService $myService)
        {
            $this->myService = $myService;
        }
    
        // Configure options
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                'action' => '/my/url/',
                'csrf_protection' => false,
                'method' => 'GET',
    
                // bind to a data class (form binding returns this object as result)
                'data_class' => Foo::class,
    
                // pass custom options to the form
                'myOption => '',
            ]);
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options): void
        {
            // Set a dynamic action
            $builder->setAction('/foo/ . $options['myOption'];
    
            $builder->add('first_name', TextType::class, [
                'required' => false,
                'label' => 'First name',
                'attr' => ['placeholder' => 'Enter your first name'],
            ]);
    
            $builder->add('secret', HiddenType::class, [
                'empty_data' => 'default',
                'required' => false,
                'label' => 'Secret',
            ]);
            // Set value for hidden
            $builder->setData(['bereich' => $options['bereich']]);
    
    
            // Use service
            $this->myService->doSomething();
    
            // Use options
            $options['myOption'];
        }
    }

 

Set Placeholder for TextType Field

Why not support 'placeholder' option like in ChoiceType?
https://github.com/symfony/symfony/issues/45214

  • $builder->add('location', TextType::class, [
        'attr' => ['placeholder' => 'Town or State)'],
    ]);

Form Builder

With a lot of options

  •     $formBuilder = $formFactory->createNamedBuilder(
            'filterForm',
            FormType::class,
            null,
            ['csrf_protection' => false,
            'attr' => ['id' => 'filter-form']
        ]);
    
        $formBuilder->setMethod('GET');
    

Controller / Action / Usage

  • namespace App\Controller;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    
    class myController extends AbstractController {
    
        public function myAction(Request $request,
                                 FormFactoryInterface $formFactory
        ) {
            // With data class 'Foo' and options
            $form = $this->createForm(FooFormType::class, $foo, [
                'myOption' => 'some value',
            ]);
    
            $form->handleRequest($request);
    
            // Or if you don't want to clear missing fields
            $form->submit($request->get($form->getName()), false);
    
            // Check for valid form
            if (!($form->isSubmitted() && $form->isValid())) {
    
                // Debugging only. Giving true to ->getErrors() is very important!!!
                dd($form->isSubmitted(), $form->isValid(), $form->getErrors(true));
                throw new \RuntimeException('Form is not submitted or not valid');
            }
    
            return $this->renderForm('myTemplate.html.twig', [
                'form' => $form,
            ]);
    
        }
    
    }

 

Do not clear missing fields

We have a form object and set some defaults, then we create the form with it.

Does not work because it clears missing fields:

  • $form->handleRequest($request);

Use instead:

  • $form->submit($request->get($form->getName()), false);

 

Use custom prefix or without prefix / form name

With custom form name / prefix

If set to empty string '' -> <input name="first_name"> instead of <input name="foo[first_name]">

  • use Symfony\Component\Form\FormFactoryInterface;
    
    public function myAction(Request $request, FormFactoryInterface $formFactory ) {
    
        $form = $this->formFactory->createNamed('', FooType::class);
    }

Wishlist

  • Allow to change values in bound form (e.g. update "offset" pagination field)
  • Allow to set form name (field prefix) in config:
    •     public function configureOptions(OptionsResolver $resolver)
          {
              $resolver->setDefaults([
                  'name' => 'my_prefix',
    • Renders: <input name="my_prefix[first_name]">
  • Support 'placeholder' option in all suitable form field types (like in ChoiceType)
  • Form Twig functions are cumbersome for custom attributes like CSS classes -> no IDE autocompletion e.g. for Bootstrap
  • Allow to get the form as http url / query similar to JS
    • new URLSearchParams(newFormData(formElement))
    • or PHP http_build_query()
      • $form->getHttpParams()

Handle CKEditor Upload Data

  • src/Form/Type/CKEditorFormType.php
    • <?php
      
      namespace App\Form\Type;
      
      use Symfony\Component\Form\AbstractType;
      use Symfony\Component\Form\Extension\Core\Type\FileType;
      use Symfony\Component\Form\FormBuilderInterface;
      use Symfony\Component\OptionsResolver\OptionsResolver;
      use Symfony\Component\Validator\Constraints\File;
      
      class CKEditorFormType extends AbstractType
      {
      
          public function configureOptions(OptionsResolver $resolver)
          {
              $resolver->setDefaults([
                  'csrf_protection' => false,
              ]);
          }
      
          public function buildForm(FormBuilderInterface $builder, array $options): void
          {
              $builder->add('upload', FileType::class, [
                 'required' => false,
                  'constraints' => [
                      new File([
                          'maxSize' => '2048k',
                          'mimeTypes' => [
                              'image/jpeg',
                              'image/x-png',
                          ],
                          'mimeTypesMessage' => 'Please upload a valid image',
                      ])
                  ],
              ]);
      
          }
      
      }
  • Controller:
    •     public function CkEditorUpload(Request $request, FormFactoryInterface $formFactory)
          {
              //create form with empty name/prefix to handle plain POST variables
              $form = $this->formFactory->createNamed('', CKEditorFormType::class);
      
              $form->handleRequest($request);
      
              if ($form->isSubmitted() && $form->isValid()) {
                  $file = $form->get('upload')->getData();
      
                  ... validate and process file
              }

Use validator constraints for file upload

  • Controller:
    • // Inject ValidatorInterface $validator
      
      /* @var UploadedFile $file */
       $file = $request->files->get('upload');
      
       if ($file) {
                  $violations = $validator->validate($file, new Image([
                      'maxSize' => '8192k',
                      'mimeTypes' => [
                          'image/jpeg',
                          'image/x-png',
                      ],
                  ]));
                  if (count($violations)) {
                      $response = new Response($violations[0]->getMessage(), 400);
      
                      return $response;
                  }
      
                  $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
                  $safeFilename = $slugger->slug($originalFilename);
                  $newFilename = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
      
                  try {
                      $uploadDir = $kernel->getProjectDir() . '/public/uploads';
                      $file->move( $uploadDir, $newFilename);
                  } catch (FileException $e) {
                      $response = new Response('Error: uploaded file could not be saved.', 400);
      
                      return $response;
                  }

Custom Validator Constraints

Constraints also work well for conditional validation.

e.g. cross check if a username exists in the other of two tables:

  • src/Validator/Constraints/UniqueUsername.php
    • <?php
      namespace App\Validator\Constraints;
      
      use Symfony\Component\Validator\Constraint;
      
      /**
      * @Annotation
      */
      class UniqueUsername extends Constraint
      {
          public $message = 'The username "{{ value }}" is already in use in table {{ entity }}.';
      }
  • src/Validator/Constraints/UniqueUsernameValidator.php

    • <?php
      
      namespace App\Validator\Constraints;
      
      use App\Entity\Foo;
      use App\Entity\Bar;
      use Symfony\Component\Validator\Constraint;
      use Symfony\Component\Validator\ConstraintValidator;
      use Doctrine\ORM\EntityManagerInterface;
      
      /**
       * Cross check for Bar <-> Foo if username exists in other table and if so add a validation error
       */
      class UniqueUsernameValidator extends ConstraintValidator
      {
          private $entityManager;
      
          public function __construct(EntityManagerInterface $entityManager)
          {
              $this->entityManager = $entityManager;
          }
      
          public function validate($value, Constraint $constraint)
          {
              $username = $value;
              $entity = $this->context->getObject();
      //        dd($username, $entity, get_class($entity));
      
              if ("App\Entity\Bar" == get_class($entity)) {
                  $Foo = $this->entityManager->getRepository(Foo::class)->findOneBy(['username' => $username]);
      
                  if ($Foo) {
                      // If the check fails, use the following method to add a validation error to the field
                      $this->context->buildViolation($constraint->message)
                          ->setParameter('{{ value }}', $value)
                          ->setParameter('{{ entity }}', 'Foo')
                          ->addViolation();
                  }
              }
      
              if ("App\Entity\Foo" == get_class($entity)) {
                  $Bar = $this->entityManager->getRepository(Bar::class)->findOneBy(['username' => $username]);
      
                  if ($Bar) {
                      // If the check fails, use the following method to add a validation error to the field
                      $this->context->buildViolation($constraint->message)
                          ->setParameter('{{ value }}', $value)
                          ->setParameter('{{ entity }}', 'Bar')
                          ->addViolation();
                  }
              }
          }
      }
  • src/Entity/Foo.php

    • /**
       * @var string|null
       *
       * @ORM\Column(name="username", type="string", length=30, nullable=true, unique=true)
       *
       * Cross check if username already exists in entity "Bar":
       * @UniqueUsername
       */
      private $username;
  • Repeat the same for entity "Bar"

     

     

     

     
    •