1 of 33

Symfony2 Forms Component

3 technieken om sneller te werk te gaan

Bart van den Burg

bart@samson-it.nl

2 of 33

Welkom!

  • Wie ben ik?

  • Types extenden
  • Autocomplete
  • Dynamische (AJAX-)formulieren

  • Interessante bundles en symfony issues

3 of 33

Wie ben ik

Op basisschool begonnen met QBasic

Op mijn 15e boek "Perl in 24 uur" gekocht

2 jaar later met PHP begonnen

En toen ging ik studeren...

Slavische Talen & Culturen

Na de studie op zoek naar een baan

Begonnen met CodeCharge

Maar sinds Symfony2 stable uit is werken we daar voor nieuwe projecten mee:

  • Roompot urenregistratie en budgettering
  • RAI flexwerkersadministratie en kledinguitgifte
  • Een narrowcasting systeem

TVReclames.nl

4 of 33

Types extenden

Probleemstelling

Elke keer als ik op mijn pagina een persoon wil laten selecteren, moet ik opnieuw die query-builder opzetten

CODE DUPLICATION

Oplossing:

Een eigen, herbruikbare type maken die deze query-builder opbouwt

5 of 33

Types extenden

Voorbeeld

Pagina 1: project aanmaken

Pagina 2: persoon "werkzaam bij" kiezen

6 of 33

CODE!!!

7 of 33

Types extenden

Uitgangspunt

Entity type toevoegen op basis van een query builder:

class ProjectType extends AbstractType{ public function __construct(Service $someService) { $this->someService = $someService; }�� /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $service = $this->someService;�� $builder->add('company', 'entity', array( 'class' => 'FooBundle:Company', 'query_builder' => function(EntityManager $em) use ($service) { return $em->createQueryBuilder('c')->where('c.field = ?1') ->setParameter(1, $service->getValue()); }, 'multiple' => true )); }}

8 of 33

Types extenden

Code duplication

Probleem zodra we ook in een andere Type een company willen selecteren:

class PersonType extends AbstractType{ public function __construct(Service $someService) { $this->someService = $someService; }�� /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $service = $this->someService;�� $builder->add('employedAt', 'entity', array( 'class' => 'FooBundle:Company', 'query_builder' => function(EntityManager $em) use ($service) { return $em->createQueryBuilder('c')->where('c.field = ?1') ->setParameter(1, $service->getValue()); }, 'multiple' => true )); }}

9 of 33

Types extenden

Ideale situatie

We hebben speciale company type nodig, zodat onze types alleen dit nog overhouden:

class ProjectType extends AbstractType{ /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $builder->add('company', 'company_selector'); }}��class PersonType extends AbstractType{ /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $builder->add('employedAt', 'company_selector'); }}

10 of 33

Types extenden

Foute aanpak

class CompanySelectorType extends AbstractType{ public function __construct(Service $someService) { $this->someService = $someService; }�� public function getName() { return 'company_selector'; }�� public function buildForm(FormBuilder $builder, array $options) { $service = $this->someService;�� $builder->add('company', 'entity', array( 'class' => 'FooBundle:Company', 'virtual' => true, 'query_builder' => function(EntityManager $em) use ($service) { return $em->createQueryBuilder('c')->where('c.field = ?1') ->setParameter(1, $service->getValue()); }, 'multiple' => true )); }}

11 of 33

Types extenden

Goede aanpak

class CompanySelectorType extends AbstractType{ public function __construct(Service $someService) { $this->someService = $someService; }�� public function getName() { return 'company_selector'; }�� public function setDefaultOptions(OptionsResolverInterface $resolver) { $service = $this->someService;�� $resolver->setDefaults(array( 'class' => 'FooBundle:Company', 'query_builder' => function(EntityManager $em) use ($service) { return $em->createQueryBuilder('c')->where('c.field = ?1') ->setParameter(1, $service->getValue()); }, 'multiple' => true )); }�� public function getParent() { return 'entity'; }}

Géén buildForm methode!

Maar wel een getParent methode!

12 of 33

Types extenden (OptionsResolver)

Wat als we toch variatie willen?

class ProjectType extends AbstractType{ /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $builder->add('company', 'company_selector', array( 'only_confirmed_companies' => true )); }}��class PersonType extends AbstractType{ /* ... */�� public function buildForm(FormBuilder $builder, array $options) { $builder->add('employedAt', 'company_selector'); }}

13 of 33

Types extenden (OptionsResolver)

Mogelijkheid 1: nieuwe optie

class CompanySelectorType extends AbstractType{ /* ... */�� public function setDefaultOptions(OptionsResolverInterface $resolver) { $service = $this->someService;�� $resolver->setDefaults(array( 'class' => 'FooBundle:Company', 'only_confirmed_companies' => false, 'query_builder' => function(Options $options) { $onlyConfirmed = $options['only_confirmed_companies']; return function(EntityManager $em) use ($service, $onlyConfirmed) { $qb = $em->createQueryBuilder('c')->where('c.field = ?1') ->setParameter(1, $service->getValue()); if ($onlyConfirmed) { $qb->andWhere('c.confirmed IS NOT NULL'); } return $qb; }; }, 'multiple' => true )); }}

OptionsResolver component!

Lazy options (type-hint is essentieel)

14 of 33

Types extenden (OptionsResolver)

Mogelijkheid 2: nogmaals extenden

Mogelijkheid 2: We extenden onze eigen type:

class ConfirmedCompanySelectorType extends AbstractType{�� public function getName() { return 'confirmed_company_selector'; }�� public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'query_builder' => function(Options $options, $value) { return $value->andWhere('c.confirmed IS NOT NULL'); }, )); }�� public function getParent() { return 'company_selector'; }}

Weer een lazy option

Deze keer op basis van de waarde van de parent

15 of 33

Autocomplete

Probleemstelling

Probleem met EntityType:

  • Alle mogelijke keuzes worden naar de client gestuurd (1000 personen in je database? +/- 40 kB & zware rendertijd voor je browser)
  • In een normale listbox valt niet makkelijk te zoeken

16 of 33

Autocomplete

Voorbeeld

17 of 33

Autocomplete

Select2

Oplossing voor probleem 2 is eenvoudig: Select2

Client side zoeken

18 of 33

Autocomplete

AutocompleteBundle

Oplossing voor probleem 1:

SamsonIT/AutocompleteBundle

Ook Select2, maar dan server-side zoeken, prima integratie met Symfony2

$ php composer.phar require samson/autocomplete-bundle

19 of 33

CODE!!!

20 of 33

Autocomplete

Voorbeeld Type

<?php

class PersonSelectorType extends AbstractType

{

public function getName() { return 'person_selector'; }

public function getParent() { return 'autocomplete'; }

public function setDefaultOptions(OptionsResolverInterface $resolver)

{

$resolver->setDefaults(array(

'class' => 'SamsonShowCaseBundle:Person',

'search_fields' => array('p.firstName', 'p.lastName', 'c.name'),

'template' => 'SamsonShowCaseBundle:Autocomplete:person_autocomplete.html.twig',

'query_builder' => function(EntityRepository $er) {

return $er->createQueryBuilder('p')

->leftJoin('p.company', 'c')

->addSelect('c')

;

}

));

}

}

21 of 33

Autocomplete

Voorbeeld template

{% if highlight %}

{{ result.firstName | highlight }} {{ result.lastName | highlight }}<br>

Company: {{ result.company | highlight }}

{% else %}

{{ result.firstName }} {{ result.lastName }}

{% endif %}

22 of 33

Dynamische formvelden

Overzicht

  • Velden toevoegen vanuit een listener
  • Probleem: verschil in preSetData en preBind
  • Verschillende manieren om POST-data om te zetten
  • Nieuwe velden client-side laden

23 of 33

Dynamische formvelden

Voorbeeld

24 of 33

CODE!!!

25 of 33

Dynamische formvelden

Velden toevoegen vanuit een listener

/* ... */

public function buildForm(FormBuilderInterface $builder)

{

$builder->add('country', 'entity', array(/* ... */));

$factory = $builder->getFormFactory();

$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $e) use ($factory) {

$country = $e->getData()->getCountry();

if (null === $country) {

$options = array('choices' => array(), 'empty_value' => 'Choose a country first');

} else {

$options = array(

'choice_list' => new ObjectChoiceList($country->getProvinces()),

'empty_value' => 'Choose a province'

);

}

$form->add($factory->createNamed('province', 'entity', null, $options);

});

}

/* ... */

De FormFactory kan losse velden genereren

We kunnen de opties genereren op basis van de waarde uit een ander veld

Het resultaat van de call naar de factory kunnen we simpelweg aan het formulier toevoegen

26 of 33

Dynamische formvelden

Verschillende dataformaten in event

FormEvent::PRE_SET_DATA

  • Entiteit (of array)
  • Valide

FormEvent::PRE_BIND

  • Platte array
  • Niet gevalideerd

FormEvent::POST_SET_DATA

  • Entiteit (of array)
  • Valide
  • Velden die nog worden toegevoegd, worden niet meer gemapped!

FormEvent::BIND / FormEvent::POST_BIND

  • Entiteit (of array)
  • Deels gevalideerd (transformers)
  • Velden die nog worden toegevoegd, worden niet meer gebound!

27 of 33

Dynamische formvelden

POST-data omzetten (1)

/* ... */

private $em;

public function __construct(EntityManager $em) {

$this->em = $em;

}

public function buildForm(FormBuilderInterface $builder)

{

/* ... */

$factory = $builder->getFormFactory();

$builder->addEventListener(FormEvents::PRE_SET_DATA, /* ... */);

$em = $this->em;

$builder->addEventListener(FormEvents::PRE_BIND, function(FormEvent $e) use ($factory, $em) {

$data = $e->getData();

$country_id = $data['country'];

if (null !== ($country = $em->find('Country', $country_id))) {

$form->add($factory->createNamed('province', 'entity', null, array(

'choice_list' => new ObjectChoiceList($country->getProvinces()),

'empty_value' => 'Choose a province'

));

}

});

}

/* ... */

$e->getData() is een simpele array

Als er al een province veld bestond, wordt deze nu simpelweg overschreven door deze nieuwe.

Is er nog geen land gekozen, dan blijft de lege listbox met "Choose a country first" gewoon staan

Gevaarlijk!

Als het country veld om willekeurige reden een bepaald land niet laat zien, kunnen met data injectie toch de provinces uitgelezen worden

Betere voorbeelden:

  • Bedrijf + prijsafspraken
  • Persoon + agendaafspraken

28 of 33

Dynamische formvelden

POST-data omzetten (2)

/* ... */

public function buildForm(FormBuilderInterface $builder)

{

/* ... */

$builder->addEventListener(FormEvents::PRE_SET_DATA, /* ... */);

$builder->addEventListener(FormEvents::PRE_BIND, function(FormEvent $e) use ($factory) {

$data = $e->getData();

$country = $data['country'];

foreach ($this->form->get('country')->getClientTransformers() as $transformer) {

$country = $transformer->reverseTransform($country);

}

foreach ($this->form->get('country')->getNormTransformers() as $transformer) {

$country = $transformer->reverseTransform($country);

}

if (null !== $country) {

$form->add($factory->createNamed('province', 'entity', null, array(

'choice_list' => new ObjectChoiceList($country->getProvinces()),

'empty_value' => 'Choose a province'

));

}

});

}

/* ... */

De ID van een country die niet selecteerbaar is in de country field, zal resulteren in een TransformationFailedException!

29 of 33

Dynamische formvelden

Client-side formulier updaten

SamsonDynamicFormBundle !!!

https://github.com/SamsonIT/DynamicFormBundle

Nog niet af...

  • Geef via de form optie 'ajax_reload' aan dat een change op je data een ajax-post (zonder persist) moet triggeren
  • Geef met de form optie 'ajax_template' op je root form aan welke form_theme gebruikt moet worden
  • Een js library die met AJAX je formulier (en niet je hele pagina) automatisch voor je reloadt
  • Handige abstracte Listener klasse die de vorige sheet voor je uit handen neemt
  • Schrijf 1 simpele methode om je dynamische velden toe te voegen

public function handleState(FormEvent $event)

{

if (null !== ($country = $this->getValue('country'))) {

$this->add('province', 'entity', null, array(

'ajax_reload' => true,

'choice_list' => new ObjectChoiceList($country->getProvinces()),

'empty_value' => 'Choose a province'

));

}

}

AJAX-reload within AJAX-reload! Inception...

30 of 33

Dynamische formvelden

Valkuilen

  • Volgorde van toevoegen is erg belangrijk
  • Bij verwijderen veld in PRE_BIND moet ook de key uit de $event->getData() worden verwijderd ($event->setData())
  • Errors kunnen niet gemapt worden aan velden die nog niet op je formulier staan�Bovenaan je formulier: "Deze waarde is niet geldig"

31 of 33

Dynamische formvelden

Issue 5807

https://github.com/symfony/symfony/issues/5807

$builder->_if(function (FormInterface $form) {� return $form->get('field1')->getData() >= 1&& !$form->get('field2')->getData();� })� ->add('myfield', 'entity', function(FormInterface $form) {� return array('query_builder' => function (EntityRepository $repo) use ($data) {� return $repo->createQueryBuilder('s')

->where('s.relation = ?1')->setParameter(1, $data);� });� })� ->add('myfield', 'text', array('label' => 'something not dependant'));� ->_endif()�;

Conditioneel velden toevoegen

Options opbouwen op basis van data van andere velden

32 of 33

Conclusie

  • Door types te extenden kan je jezelf veel werk uit handen nemen
  • Met de OptionsResolver kan je opties uit parent types gebruiken, overriden of uitbreiden
  • De AutocompleteBundle is een handige tool om makkelijk een autocomplete in je forms te hangen
  • Dynamisch formvelden toevoegen is een ingewikkelde klus vol valkuilen, maar stelt je in staat flexibele formulieren op te bouwen
    • De DynamicFormBundle helpt je op weg
    • Issue 5807 brengt eindeloze mogelijkheden

33 of 33

Vragen?

Bart van den Burg

Samson IT

bart@samson-it.nl

https://github.com/Burgov