Symfony2 Forms Component
3 technieken om sneller te werk te gaan
Bart van den Burg
bart@samson-it.nl
Welkom!
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:
TVReclames.nl
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
Types extenden
Voorbeeld
Pagina 1: project aanmaken
Pagina 2: persoon "werkzaam bij" kiezen
CODE!!!
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� ));� }�}
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� ));� }�}
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');� }�}
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� ));� }�}
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!
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');� }�}
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)
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
Autocomplete
Probleemstelling
Probleem met EntityType:
Autocomplete
Voorbeeld
Autocomplete
Select2
Oplossing voor probleem 2 is eenvoudig: Select2
Client side zoeken
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
CODE!!!
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')
;
}
));
}
}
Autocomplete
Voorbeeld template
{% if highlight %}
{{ result.firstName | highlight }} {{ result.lastName | highlight }}<br>
Company: {{ result.company | highlight }}
{% else %}
{{ result.firstName }} {{ result.lastName }}
{% endif %}
Dynamische formvelden
Overzicht
Dynamische formvelden
Voorbeeld
CODE!!!
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
Dynamische formvelden
Verschillende dataformaten in event
FormEvent::PRE_SET_DATA
FormEvent::PRE_BIND
FormEvent::POST_SET_DATA
FormEvent::BIND / FormEvent::POST_BIND
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:
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!
Dynamische formvelden
Client-side formulier updaten
Nog niet af...
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...
Dynamische formvelden
Valkuilen
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
Conclusie
Vragen?
Bart van den Burg
Samson IT
bart@samson-it.nl
https://github.com/Burgov