10 th Lecture COP 4991 Component-Based Software Development Instructor: Masoud Sadjadi
Symfony CoP: Form component
-
Upload
samuel-roze -
Category
Software
-
view
440 -
download
0
Transcript of Symfony CoP: Form component
Symfony
Form component
Plan1. Basic usage
2. Validation
3. Custom types
4. Events
5. Data Transformers
6. Form type extensions
7. Rendering overview
What's the Form component?
Basic usages
Basic usage (Symfony way)
public function createAction(Request $request){ $form = $this ->createFormBuilder([]) ->add('comment', 'textarea') ->getForm() ;
$form->handleRequest($request); if ($form->isValid()) { $data = $form->getData();
// Do what ever you want with the data... $comment = $data['comment']; }
return [ 'form' => $form->createView(), ];}
Basic usage (rendering)
# create.html.twig{{ form_start(form) }} {{ form_errors(form) }}
{{ form_row(form.comment) }}
<input type="submit" name="Let's go" />
{{ form_end(form) }}
Basic usage (Object instead of array)
public function createOrUpdateAction(Request $request, MyObject $myObject = null){ $myObject = $myObject :? new MyObject();
$form = $this ->createFormBuilder($myObject, [ 'data_class' => MyObject::class, ]) ->add('comment', 'textarea') ->getForm() ;
$form->handleRequest($request); if ($form->isValid()) { // Do what ever you want with the updated object... $comment = $myObject->getComment(); }
return [ 'form' => $form->createView(), ];}
Validation
Validation
$form = $this->createFormBuilder() ->add('comment', 'textarea', [ 'required' => true, 'constraints' => [ new NotBlank(), ], ]) ->getForm();
Basic usage (Validation on the object)
use Symfony\Component\Validator\Constraints as Assert;
class MyObject{ /** * @Assert\NotBlank * @Assert\Length( * min=10, * minMessage="The comment have to be useful" * ) */ private $comment;
// Required methods public function getComment(); public function setComment($comment);}
Basic usage (Get validation errors)
...that's rendered for you!
But if you want access to them...
$form->handleRequest($request);if (!$form->isValid()) { $errors = form->getErrors();
// `$errors` is an array of `FormError` objects.}
Hey!31 built-in types
46 built-in validators
Custom types
Custom type definition
class MyFormType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('comment', 'textarea') ; }}
Using the Form Type
public function createAction(Request $request){ $form = $this->createForm(new MyFormType()); $form->handleRequest($request);
if ($form->isValid()) { $data = $form->getData();
// Do what ever you want with the data... $comment = $data['comment']; }
return [ 'form' => $form->createView(), ];}
Form Type options
class MyFormType extends AbstractType{ //!\ Replace `setDefaultOptions` since Symfony 3.0 /!\\ public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['my-custom-option']);
$resolver->setDefaults(array( 'data_class' => 'App\Model\Object', )); }}
Form Type as a service
class GenderType extends AbstractType{ private $genderChoices;
public function __construct(array $genderChoices) { $this->genderChoices = $genderChoices; }
public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'choices' => $this->genderChoices, )); }}
Form Type as a service
Register the form type<service id="app.form.type.gender" class="App\Form\Type\GenderType"> <argument>%genders%</argument> <tag name="form.type" /></service>
Use the form typeclass MyFormType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('gender', GenderType::class) ; }}
EventsOr how to dynamically update the FormType based on
data.
Form workflowIt dispatches different events while handling the requests.
» PRE_SET_DATA
» POST_SET_DATA
» PRE_SUBMIT
» SUBMIT
» POST_SUBMIT
The name field only for a new product
public function buildForm(FormBuilderInterface $builder, array $options){ // ...
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { $product = $event->getData(); $form = $event->getForm();
// The product name is only updatable for a new `Product` if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } });}
Event subscribers
public function buildForm(FormBuilderInterface $builder, array $options){ // ...
$builder->addEventSubscriber(new AddNameFieldSubscriber());}
An event subscriber
class AddNameFieldSubscriber implements EventSubscriberInterface{ public static function getSubscribedEvents() { return array(FormEvents::PRE_SET_DATA => 'preSetData'); }
public function preSetData(FormEvent $event) { $product = $event->getData(); $form = $event->getForm();
if (!$product || null === $product->getId()) { $form->add('name', TextType::class); } }}
List based on another field1. Initial Form Type
public function buildForm(FormBuilderInterface $builder, array $options){ $builder ->add('sport', EntityType::class, array( 'class' => 'AppBundle:Sport', 'placeholder' => '', )) ;}
List based on another field2. Our form modifier
$formModifier = function (FormInterface $form, Sport $sport = null) { $positions = null === $sport ? array() : $sport->getAvailablePositions();
$form->add('position', EntityType::class, array( 'class' => 'AppBundle:Position', 'placeholder' => '', 'choices' => $positions, ));};
List based on another field3. The listeners
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formModifier) { $data = $event->getData();
$formModifier($event->getForm(), $data->getSport());});
$builder->get('sport')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($formModifier) { // It's important here to fetch $event->getForm()->getData(), as // $event->getData() will get you the client data (that is, the ID) $sport = $event->getForm()->getData();
// since we've added the listener to the child, we'll have to pass on // the parent to the callback functions! $formModifier($event->getForm()->getParent(), $sport);});
Data Transformers
Normalization flow
1. Model data. Our object returned by getData().
2. Internal representation, mostly our model data. Almost never used by the developer.
3. View data. The data structure sent to submit().
Using the CallbackTransformer
public function buildForm(FormBuilderInterface $builder, array $options){ $builder->add('description', TextareaType::class);
$builder->get('description')->addModelTransformer(new CallbackTransformer(function ($originalDescription) { // transform <br/> to \n so the textarea reads easier return preg_replace('#<br\s*/?>#i', "\n", $originalDescription); }, function ($submittedDescription) { // remove most HTML tags (but not br,p) $cleaned = strip_tags($submittedDescription, '<br><br/><p>');
// transform any \n to real <br/> return str_replace("\n", '<br/>', $cleaned); }));}
An integer to an object1. Our task Form Type
class TaskType extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('description', TextareaType::class) ->add('issue', TextType::class) ; }
public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'App\Model\Task' )); }
// ...}
An integer to an object2. The transformer
class IssueToNumberTransformer implements DataTransformerInterface{ private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; }
// Two methods to implement... public function transform($issue); public function reverseTransform($issueNumber);}
An integer to an object3. From model to view
public function transform($issue){ return null !== $issue ? $issue->getId() : '';}
An integer to an object3. From view to model
public function reverseTransform($issueNumber){ if (empty($issueNumber)) { return; }
if (null === ($issue = $this->issueRepository->find($issueNumber))) { throw new TransformationFailedException(sprintf('An issue with number "%s" does not exist!', $issueNumber)); }
return $issue;}
An integer to an object4. Voilà!
class TaskType extends AbstractType{ private $issueRepository; public function __construct(IssueRepository $issueRepository) { $this->issueRepository = $issueRepository; }
public function buildForm(FormBuilderInterface $builder, array $options) { // ...
$builder->get('issue')->addModelTransformer(new IssueToNumberTransformer($this->issueRepository)); }
}
Form Type Extensions
An extension class
class IconTypeExtension extends AbstractTypeExtension{ public function getExtendedType() { return ChoiceType::class; }
// We can now declare the following methods... public function configureOptions(OptionsResolver $resolver); public function buildForm(FormBuilderInterface $builder, array $options); public function buildView(FormView $view, FormInterface $form, array $options); public function finishView(FormView $view, FormInterface $form, array $options)}
An optional extra icon
class IconTypeExtension extends AbstractTypeExtension{ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'icon' => null, ]); }
}
An optional extra icon
class IconTypeExtension extends AbstractTypeExtension{ public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['icon'] = $options['icon']; }}
You'll need to extend the choice_widget block using a form theme, so you can display an icon when the local icon variable is defined.
Register the extension
<service id="app.image_type_extension" class="App\Form\Extension\ImageTypeExtension">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\ChoiceType" />
</service>
Rendering
Using a form theme
{% form_theme form 'form/fields.html.twig' %}{# or.. #}{% form_theme form _self %}
{{ form(form) }}
Example theme
# form/fields.html.twig
{% block form_row %}{% spaceless %} <div class="form_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div>{% endspaceless %}{% endblock form_row %}
Creating a form themeUsing Twig blocks.
» [type]_row
» [type]_widget
» [type]_label
» [type]_errors
If the given block of type is not found, it will use the parent's type.
FormType's buildView
class GenderType extends AbstractType{ private $genderChoices;
public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['genders'] = $this->genderChoices; }}
FormType's buildView
# form/fields.html.twig{% block gender_widget %}{% spaceless %} {# We can use the `gender` variable #}{% endspaceless %}{% endblock %}
FormType's finishView
Called when the children's view is completed.
public function finishView(FormView $view, FormInterface $form, array $options){ $multipart = false;
foreach ($view->children as $child) { if ($child->vars['multipart']) { $multipart = true; break; } }
$view->vars['multipart'] = $multipart;}
Creating a form without name1. The difference?
Your form properties won't be namespacedExample: comment instead of my_form[comment]
You might need/want to use it for:- Legacy applications compatibility- APIs (with the FOS Rest Body Listener)
Creating a form without name2. How?
private $formFactory;
public function __construct(FormFactoryInterface $formFactory){ $this->formFactory = $formFactory;}
public function createAction(Request $request){ $form = $this->formFactory->createNamed(null, new MyFormType()); $form->handleRequest($request);
// ...}
Wow, we're done!