Showing posts with label form. Show all posts
Showing posts with label form. Show all posts

Monday, June 13, 2016

Symfony - How to apply validation based on user input using validation groups

In Symfony applications validation constraints are applied to the Entity and not to the Form. In some cases, however, you'll need to validate an object against only some constraints on that class. To do this, you can organize each constraint into one or more "validation groups", and then apply validation against just one group of constraints.

http://symfony.com/doc/current/book/validation.html#book-validation-validation-groups

Even better, you can determine which validation group should be applied based on the value filled in form by user:

http://symfony.com/doc/current/book/forms.html#groups-based-on-the-submitted-data

Let's say we have a   Product entity. Product entity has a 2 properties: description and category.
Depending on the "category "property different validation groups can be applied to 'description'. Symfony defines a group called "Default" in which are included all the validation which are not marked as part of any group.


<?php

class Product
{
    /**
     * @var string
     *
     * @ORM\Column(name="description", type="text",  nullable=false)
     * @Assert\NotNull(groups={"imported"})
     */
    protected $description;

    protected $category;
}

I marked the NotNull validation as being part of "imported" group.

Now in the form class using the Product entity I need to put the logic based on which the validation groups are used:


<?php

use AppBundle\Entity\Product;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProductType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
       ....
    }
    // ...
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'validation_groups' => function (FormInterface $form) {
                $data = $form->getData();

                if ($data->getCategory() === 'Exotic fruits' ) {
                    return array('Default', 'imported');
                }

                return array('Default');
                },
        ));
    }
}

The code above basically says :
 - if category is "Exotic fruits" than apply "Default" and "imported" validations.
- otherwise apply only "Default" validations

Sometimes you need advanced logic to determine the validation groups. If they can't be determined by a simple callback, you can use a service:   http://symfony.com/doc/current/cookbook/validation/group_service_resolver.html

Tuesday, March 15, 2016

Symfony: How to embed a Collection of Forms and customize the form field Prototype

This article is based on the following doc articles:

http://symfony.com/doc/current/cookbook/form/form_collections.html
http://symfony.com/doc/2.7/reference/forms/types/collection.html
http://toni.uebernickel.info/2012/03/15/an-example-of-symfony2-collectiontype-form-field-prototype.html

In my example below I will be using Doctrine's  One-To-Many Bidirectional relation. Let's say we have a Feedback form to which you can add none or many Actions to be taken. Feedback form contains information like name and email of the person and Action form contains information like action description, deadline date, importance level. I will eliminate any details which are not relevant to the subject (like all the mappings, validations etc)/

Entities:

Feedback
<?php

class Feedback
{
    
    /**
     * @ORM\OneToMany(targetEntity="\MyBundle\Entity\Action", mappedBy="fkFeedback", cascade={"persist","remove"})
     */
    private $actions;
    
    private $name;
    
    private $email;


    public function __construct() {
        $this->actions = new ArrayCollection();
    }

    public function getActions()
    {
        return $this->actions;
    }

    public function addAction(\MyBundle\Entity\Action $action) 
    {
        $action->setFkIdFeedback($this);
        $this->actions->add($action);
        return $this;
    }
 
    public function removeAction($action) 
    {
        if ($this->actions>contains($action)) {
            $this->actions->removeElement($action);
        }
        return $this;
    }

}

Action

<?php

class Action

{
    private $actionDesc;

    private $importance;

    private $deadlineDate;

    /**
     * @ORM\ManyToOne(targetEntity="Feedback", inversedBy="actions")
     * @ORM\JoinColumn(name="fk_feedback", referencedColumnName="id", nullable=FALSE )
     */
    private $fkFeedback;
}


Note mappedBy and inversedBy properties. Also on the One side I have added information regarding how Doctrine should persist the associated entities: cascade={'persist'}.
Also 'addAction()' function makes sure the changes are reflected in both sides of the relation.

 Forms


<?php

class FeedbackType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name','text',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Last name')))
            ->add('email','email',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Email')))
            ->add('actions', 'collection', array(
                'type' => new ActionType(),
                'allow_add'    => true,
                'allow_delete' => true,
                'by_reference' => false,
                'cascade_validation' => true,
                'error_bubbling' => false,
            ));
    }
    
    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\Feedback',
            'cascade_validation' => true,   
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'mybundle_feedback';
    }
}

On the FeedbackType class (One side from the One-To-Many relation) I set a collection of ActionType forms. Also 'error_bubbling' to false because I want to get errors from Action form under each field and not only at the form level: form_error(form) .

The ActionType class has nothig special in it, I will not listed here.

Allowing the user to dynamically add new Actions means that you'll need to use some JavaScript.
The option 'allow_add' is needed to this flexibility to be possible.

In addition to telling the field to accept any number of submitted objects, the allow_add also makes a "prototype" variable available to you. This "prototype" is a little "template" that contains all the HTML to be able to render any new "tag" forms.

If you render the entire form you will see in a HTML  data-*  attribute the prototype for creating new Actions. From here on I will not follow the example found in official documentation, instead I will render the form and use jQuery code like in this blog article.

Rendering the form

The prototype macro

In the situation when by default I want to render 2 actions to be filled and allow dynamically to be added other there may be differences between how the widget is rendered and how the prototype is rendered. The below Twig macro handles this issue.
This macro renders the prototype and the actual widget the same way. Therefore the resulting usage code is very little and required javascript code just works for all collections.


{% macro widget_prototype(widget, remove_text) %}
        {% if widget.vars.prototype is defined %}
            {% set form = widget.vars.prototype %}
            {% set name = widget.vars.prototype.vars.name %}
        {% else %}
            {% set form = widget %}
            {% set name = widget.vars.full_name %}
    {% endif %}

        <div data-content="{{ name }}"  class="panel panel-default">
            <div class="panel-body">
                {{ form_row(form.actionDesc) }}
                {{ form_row(form.importance) }}
                {{ form_row(form.deadlineDate, {'attr':{'class':'actions-deadline'}}) }}
                <a class="btn btn-danger" data-related="{{ name }}">{{ remove_text }}</a>
            </div>
            
            
        </div>
    {% endmacro %}

As an example I customized the class attribute for deadlineData to be able to identify them for adding  Datepicker jQuery UI plugin.  The values  widget.vars.full_name and widget.vars.name are related to form object not to my actual Entity fields (name, email).

The actual form rendering:

I am not rendering the form using form_start, form_end in order to get more control on what is displayed. If you are using Bootstrap you can use one of the Twig form layouts offered by Symfony team:


        {% form_theme form 'MyBundle:Form:bootstrap_3_layout.html.twig' %}
        <div class="row">
            <div class="col-sm-3"></div>
            <div class="col-sm-6">
                <h3 style="color:white;">Feedback</h3>
                <form name="docsbundle_feedback" method="POST">
                        {{ form_errors(form) }}
                    <fieldset>           
                        {{ form_row(form.name) }}
                        {{ form_row(form.email) }}
                        <hr>
                         <h3 style="color:white">Actions</h3>
                            <div>
                                <div id="actions" data-prototype="{{ _self.widget_prototype(form.actions, 'Remove action')|escape }}">
                                    {% for widget in form.actions.children %}
                                        {{ _self.widget_prototype(widget, 'Remove action') }}
                                    {% endfor %}
                                </div>

                                <a id="add-action" class="btn btn-info" data-target="actions">Add action</a>
                            </div>

                        <hr>               

                        {{ form_widget(form._token) }}

                        {{ form_widget(form.submit, {'attr':{'class':'btn-info'}}) }}

                    </fieldset>
                </form>
            </div>
        <div class="col-sm-3"></div>
        </div>

jQuery snippet:

 The below jQuery take care to allow adding and removing Actions.


{% block javascripts %}
    <script>
        $('#add-action').click(function(event) {
            var collectionHolder = $('#' + $(this).attr('data-target'));
            var prototype = collectionHolder.attr('data-prototype');
            var form = prototype.replace(/__name__/g, collectionHolder.children().length);

            collectionHolder.append(form);

            return false;
        });
        $('#actions').on('click','.btn-danger', function(event) {
            var name = $(this).attr('data-related');
            $('*[data-content="'+name+'"]').remove();

            return false;
        });

    </script>
{% endblock %}

Controller

In controller I am persisting only the Feedback entity, and with the option cascade={'persis'}, Doctrine knows to take care of persisting the associated Action objects


<?php

public function feedbackAction(Request $request)
    {
        
        $entity = new Feedback();
        
        $empty_action = new Action();
        $entity->addAction($empty_action);
        
        $form = $this->feedbackCreateForm($entity);
        
        $form->handleRequest($request);           
        
        if ($form->isValid()) {  
            .....
            // persist the $entity
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity); 
            $em->flush();
            ...

Friday, January 8, 2016

Symfony custom constaint: check total size of several form attachements


The cookbook link on how to create a custom constraint is this one:
http://symfony.com/doc/master/cookbook/validation/custom_constraint.html


Request: total size of form attachments should not be bigger than a given value.

Implementation:

I've created a directory 'Validator' under my Bundle where I added two classes:
- FilesTotalSize which extends class Constaint
- FilesTotalSizeValidator extends ConstaintValidator

FilesTotalSize:


<?php
namespace Bundle\Validator;

use Symfony\Component\Validator\Constraint;

/** @Annotation */
class FilesTotalSize extends Constraint
{
    public $message;
   
    public function validatedBy()
    {
        return get_class($this).'Validator';
    }

    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
}


The name of the Validation class will be constraint class concatenated with 'Validator';

Because this constraint is checking values from several fields is considered a class validation (as opposite to field validation which can be seen in the documentation examples ).
For this reason getTargets will return CLASS_CONSTRAINT.

The actual check of the condition is made in the validator class. The variable '$value' will give access to the object to be validated. 'attachement1', 'attachement2' and 'attachement3' are the names of the fields declared in the Entity used for file uploading (file). The size is expressed in bytes, so for 3MB I have 3 millions of bytes. The method  'addViolation' receive as parameter the error message to be displayed.
.
FilesTotalSizeValidator:

<?php
namespace  Bundle\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;


class FilesTotalSizeValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        $size_att1=0;
        $size_att2=0;
        $size_att3=0;
       
        if (property_exists($value, 'attachement1')){
            if(!is_null($value->getAttachement1())){
                $size_att1= $value->getAttachement1()->getClientSize();
            }
        }
       
        if (property_exists($value, 'attachement2')){
            if(!is_null($value->getAttachement2())){
                $size_att2= $value->getAttachement2()->getClientSize();
            }
        }
       
        if (property_exists($value, 'attachement3')){
            if(!is_null($value->getAttachement3())){
                $size_att3= $value->getAttachement3()->getClientSize();
            }
        }
       
        $totalSize=0;
        $totalSize=(int)$size_att1 +(int)$size_att2 +(int)$size_att3;

        if ($totalSize > 3145728) {  //check if total size of files is bigger than 3MB
            $this->context->addViolation('Maximum size is 3MB!');
        }
    }
}



Usage:

In the entity class:

use MyBundle\Validator\FilesTotalSize;

/**
 * @ORM\Entity
 * @FilesTotalSize
 */


class EntityName {