Showing posts with label validation. Show all posts
Showing posts with label validation. Show all posts

Thursday, July 21, 2016

Using the Symfony Validator component as a standalone library

Sometimes I need to build some scripts without using a full framework like Symfony. The good news is that you can use individual components from Symfony. Most often I will be using the HTTPFoundation component, but for today I want to build a CLI script so no need for HTTPFoundation.
I  will be writing a small command line script to read and validate a  CSV file. For the  validation part I will be using the Symfony Validator Component,

Source of inspiration: https://blog.tinned-software.net/using-the-symfony-validator-as-a-standalone-component/


First step, install Symfony Validator using Composer:

                          composer require symfony/validator

and require the autoloader created by Composer.


<?php

require_once 'vendor/autoload.php';
require_once 'Loader.php';

use Symfony\Component\Validator\Validation;

$file = "someFile.csv";


$outputFile = "errorsLog" . "_" . time() . ".txt";
  
$validator = Validation::createValidatorBuilder()
 ->addMethodMapping('loadValidatorMetadata')
 ->getValidator()
;

$loader = new Loader($file, $outputFile, $validator);
$loader->load();

I will pass to my Loader class constructor the CSV file to be read, the file where I want to save the output and an instance of the Symfony validator.

The CSV file is read line by line, and from each line I will be creating a Row object. The validator will validate the Row object against the rules (constraints).
In the Row class I will add the method "loadValidatorMetada" mentioned when instantiating the Validator. I've added several validations for better examplification:


<?php

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;


class Row
{
    private $id;         
    private $name;   
    private $personalNumber;    
    private $CountryCode;  

    public function __construct($rowNumber, $data)
    {
        $this->id = $rowNumber;
        $this->name= $data[0];
        $this->personalNumber = $data[2];
        $this->countryCode = $data[1];
    }
 
 
    /**
     * This method is where you define your validation rules.
    */
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        //Name 
        $metadata->addPropertyConstraint('name', new Assert\NotBlank());
 
        //Personal number
        $metadata->addPropertyConstraint('personalNumber', new Assert\NotBlank());
        $metadata->addPropertyConstraint('personalNumber', new Assert\Type(array(
            'type'    => 'digit',
            'message' => 'The value {{ value }} is not a valid personal number.',
         )));
  
         //Country code
         $metadata->addPropertyConstraint('countryCode', new Assert\NotBlank());
         $metadata->addPropertyConstraint('countryCode', new Assert\Choice(array(
            'choices' => array('DE', 'AT', 'LU', 'ES', 'FR', 'BE', 'NO', 'SI', 'SE', 'IT', 'DK'),
            'message' => '{{ value }} is not a valid country code!',
          )));
  
     }
}


When validating a Row object against these constraints an array of Errors will be returned by the Validator. Below is the Loader class.



<?php

require_once 'Row.php';


class Loader
{
 private $fileName;
 private $validator;
 private $outputFile;
 
 public function __construct($fileName, $outputFile, $validator)
 {
  $this->fileName = $fileName;
  $this->outputFile = $outputFile;
  $this->validator = $validator;

 }
 
 public function load()
 {
  $file_handle = fopen($this->fileName, "r");
  $fileOutputHandle = fopen($this->outputFile, "w");
  
  /* 
   * Keep track of the current row in .csv file
   */
  $rowNumber=1;
  
  /* 
   * Read the file line by line
   */
  while (!feof($file_handle) ) {

   $line_of_text = fgetcsv($file_handle, 0);
   
   if ($rowNumber === 1) {
    
      /*
       * Ignore the first row from file as it contains headers
       */
     
   } else {
       $row = new Row($rowNumber, $line_of_text);    
       $this->validateRow($row, $fileOutputHandle); 
   }
   
   $rowNumber++;
  }
  fclose($file_handle);
  fclose($fileOutputHandle);
 }
 
 
 public function validateRow(Row $row, $fileOutputHandle)
 {
  $errors = $this->validator->validate($row);
  
  foreach ($errors as $error) {
   $errorMsg = "At row:" 
      . $error->getRoot()->getId() 
      . "- Property: " 
      . $error->getPropertyPath()
      . ' - Message: ' 
      . $error->getMessage()
      . "\n";
   
   fwrite($fileOutputHandle, $errorMsg);

  }
 }
}

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