1
votes

I don't understand why some constraints does not insert name of property in error message after validation. I have this entity class:

 <?php


namespace AC\OperaBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use AC\UserBundle\Entity\Utente;
use Symfony\Component\Validator\Constraints as Assert;


/**
 * Class Episodio
 * @package AC\OperaBundle\Entity
 * @ORM\Entity(repositoryClass="AC\OperaBundle\Repository\EpisodioRepository")
 * * @ORM\Table(
 *      name="ac_Episodio",
 *      uniqueConstraints={@ORM\UniqueConstraint(name="unique_idx", columns={"opera", "numero_episodio", "extra"})},
 *      indexes={   @ORM\Index(name="opera_idx", columns={"opera"}),
 *                  @ORM\Index(name="numero_episodio_idx", columns={"numero_episodio"}),
 *                  @ORM\Index(name="extra_idx", columns={"extra"}),
 *                  @ORM\Index(name="attivo_idx", columns={"attivo"}) })
 */
class Episodio {

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var Opera
     *
     * @ORM\ManyToOne(targetEntity="AC\OperaBundle\Entity\Opera", inversedBy="episodi")
     * @ORM\JoinColumn(name="opera", referencedColumnName="id", nullable=false)
     */
    protected $opera;

    /**
     * @var string
     *
     * @ORM\Column(name="numero_episodio", type="string", length=5, nullable=false)
     */
    protected $numero_episodio;

    /**
     * @var string
     *
     * @ORM\Column(name="extra", type="string", length=30, nullable=false)
     */
    protected $extra;

    /**
     * @var string
     *
     * @ORM\Column(name="titolo_italiano", type="string", length=150, nullable=false)
     *  @Assert\NotBlank()
     */
    protected $titolo_italiano;

    /**
     * @var string
     *
     * @ORM\Column(name="titolo_originale", type="string", length=150, nullable=false)
     *  @Assert\NotBlank()
     */
    protected $titolo_originale;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="data_ita", type="date", nullable=true)
     * @Assert\Date()
     */
    protected $data_ita;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="data_jap", type="date", nullable=true)
     * @Assert\Date()
     */
    protected $data_jap;

    /**
     * @var int
     *
     * @ORM\Column(name="durata", type="smallint", nullable=false, options={"default" = 0})
     */
    protected $durata;

    /**
     * @var string
     *
     * @ORM\Column(name="trama", type="text", nullable=false)
     */
    protected $trama;

    /**
     * @var int
     *
     * @ORM\Column(name="ordine", type="smallint", nullable=false, options={"default" = 0})
     */
    protected $ordine;

    /**
     * @var int
     *
     * @ORM\Column(name="promosso", type="integer", nullable=false, options={"default" = 0})
     */
    protected $promosso;

    /**
     * @var int
     *
     * @ORM\Column(name="rimandato", type="integer", nullable=false, options={"default" = 0})
     */
    protected $rimandato;

    /**
     * @var int
     *
     * @ORM\Column(name="bocciato", type="integer", nullable=false, options={"default" = 0})
     */
    protected $bocciato;

    /**
     * @var boolean
     *
     * @ORM\Column(name="fansub", type="boolean", nullable=false)
     */
    protected $fansub;

    /**
     * @var boolean
     *
     * @ORM\Column(name="attivo", type="boolean", nullable=false)
     */
    protected $attivo;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="data_aggiornamento", type="date")
     */
    protected $data_aggiornamento;

    /**
     * @var Utente
     *
     * @ORM\ManyToOne(targetEntity="AC\UserBundle\Entity\Utente")
     * @ORM\JoinColumn(name="utente_aggiornamento", referencedColumnName="id", nullable=true)
     */
    protected $utente_aggiornamento;

    /**
     * @param boolean $attivo
     */
    public function setAttivo($attivo)
    {
        $this->attivo = $attivo;
    }

    /**
     * @return boolean
     */
    public function getAttivo()
    {
        return $this->attivo;
    }

    /**
     * @param int $bocciato
     */
    public function setBocciato($bocciato)
    {
        $this->bocciato = $bocciato;
    }

    /**
     * @return int
     */
    public function getBocciato()
    {
        return $this->bocciato;
    }

    /**
     * @param \DateTime $data_aggiornamento
     */
    public function setDataAggiornamento($data_aggiornamento)
    {
        $this->data_aggiornamento = $data_aggiornamento;
    }

    /**
     * @return \DateTime
     */
    public function getDataAggiornamento()
    {
        return $this->data_aggiornamento;
    }

    /**
     * @param \DateTime $data_ita
     */
    public function setDataIta($data_ita)
    {
        $this->data_ita = $data_ita;
    }

    /**
     * @return \DateTime
     */
    public function getDataIta()
    {
        return $this->data_ita;
    }

    /**
     * @param \DateTime $data_jap
     */
    public function setDataJap($data_jap)
    {
        $this->data_jap = $data_jap;
    }

    /**
     * @return \DateTime
     */
    public function getDataJap()
    {
        return $this->data_jap;
    }

    /**
     * @param int $durata
     */
    public function setDurata($durata)
    {
        $this->durata = $durata;
    }

    /**
     * @return int
     */
    public function getDurata()
    {
        return $this->durata;
    }

    /**
     * @param string $extra
     */
    public function setExtra($extra)
    {
        $this->extra = $extra;
    }

    /**
     * @return string
     */
    public function getExtra()
    {
        return $this->extra;
    }

    /**
     * @param boolean $fansub
     */
    public function setFansub($fansub)
    {
        $this->fansub = $fansub;
    }

    /**
     * @return boolean
     */
    public function getFansub()
    {
        return $this->fansub;
    }

    /**
     * @param int $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param string $numero_episodio
     */
    public function setNumeroEpisodio($numero_episodio)
    {
        $this->numero_episodio = $numero_episodio;
    }

    /**
     * @return string
     */
    public function getNumeroEpisodio()
    {
        return $this->numero_episodio;
    }

    /**
     * @param \AC\OperaBundle\Entity\Opera $opera
     */
    public function setOpera($opera)
    {
        $this->opera = $opera;
    }

    /**
     * @return \AC\OperaBundle\Entity\Opera
     */
    public function getOpera()
    {
        return $this->opera;
    }

    /**
     * @param int $ordine
     */
    public function setOrdine($ordine)
    {
        $this->ordine = $ordine;
    }

    /**
     * @return int
     */
    public function getOrdine()
    {
        return $this->ordine;
    }

    /**
     * @param int $promosso
     */
    public function setPromosso($promosso)
    {
        $this->promosso = $promosso;
    }

    /**
     * @return int
     */
    public function getPromosso()
    {
        return $this->promosso;
    }

    /**
     * @param int $rimandato
     */
    public function setRimandato($rimandato)
    {
        $this->rimandato = $rimandato;
    }

    /**
     * @return int
     */
    public function getRimandato()
    {
        return $this->rimandato;
    }

    /**
     * @param string $titolo_italiano
     */
    public function setTitoloItaliano($titolo_italiano)
    {
        $this->titolo_italiano = $titolo_italiano;
    }

    /**
     * @return string
     */
    public function getTitoloItaliano()
    {
        return $this->titolo_italiano;
    }

    /**
     * @param string $titolo_originale
     */
    public function setTitoloOriginale($titolo_originale)
    {
        $this->titolo_originale = $titolo_originale;
    }

    /**
     * @return string
     */
    public function getTitoloOriginale()
    {
        return $this->titolo_originale;
    }

    /**
     * @param string $trama
     */
    public function setTrama($trama)
    {
        $this->trama = $trama;
    }

    /**
     * @return string
     */
    public function getTrama()
    {
        return $this->trama;
    }

    /**
     * @param \AC\UserBundle\Entity\Utente $utente_aggiornamento
     */
    public function setUtenteAggiornamento($utente_aggiornamento)
    {
        $this->utente_aggiornamento = $utente_aggiornamento;
    }

    /**
     * @return \AC\UserBundle\Entity\Utente
     */
    public function getUtenteAggiornamento()
    {
        return $this->utente_aggiornamento;
    }


}

In the controller perform the classi call at the $form->isValid() method to check validation. If there are errors i call $form->getErrorsAsString() and this is the result:

ERROR: This value should not be blank.
ERROR: This value should not be blank.
numeroEpisodio:
    No errors
titoloItaliano:
    No errors
titoloOriginale:
    No errors
dataIta:
    ERROR: This value is not valid.
dataJap:
    ERROR: This value is not valid.
durata:
    No errors
trama:
    No errors
extra:
    No errors
fansub:
    No errors

The property that use @Assert\NotBlank() dose not put property name in error message! And for this reason i have the first two error line with e generic:

ERROR: This value should not be blank.
ERROR: This value should not be blank.

I i don't know who is the property that failed the validation. I look in the source code of Symfony Component and i see that for not blank constraint:

/**
 * @author Bernhard Schussek <[email protected]>
 *
 * @api
 */
class NotBlankValidator extends ConstraintValidator
{
    /**
     * {@inheritdoc}
     */
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof NotBlank) {
            throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\NotBlank');
        }

        if (false === $value || (empty($value) && '0' != $value)) {
            $this->context->addViolation($constraint->message);
        }
    }
}

And for data constraint:

/**
 * @author Bernhard Schussek <[email protected]>
 *
 * @api
 */
class DateValidator extends ConstraintValidator
{
    const PATTERN = '/^(\d{4})-(\d{2})-(\d{2})$/';

    /**
     * {@inheritdoc}
     */
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof Date) {
            throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Date');
        }

        if (null === $value || '' === $value || $value instanceof \DateTime) {
            return;
        }

        if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
            throw new UnexpectedTypeException($value, 'string');
        }

        $value = (string) $value;

        if (!preg_match(static::PATTERN, $value, $matches) || !checkdate($matches[2], $matches[3], $matches[1])) {
            $this->context->addViolation($constraint->message, array('{{ value }}' => $value));
        }
    }
}

The important difference is $this->context->addViolation($constraint->message); VS $this->context->addViolation($constraint->message, array('{{ value }}' => $value));

Why?

This is core part of controller

 if ($request->getMethod() == 'POST') {

            try {
                $form->handleRequest($request);
                if ($form->isValid()) {
                    /* @var $item Episodio */
                    $item = $form->getData();
                    $em->persist($item);
                    $em->flush();
                    $msg = new Message(true, Message::OK_MESSAGE);
                } else {
                    $msg = new Message(false, Message::KO_MESSAGE);
                    $errors = Utility::getErrorMessages($form);
                    $msg->setData($errors);
                }
            } catch (\Exception $ex) {
                $msg = new Message(false, $ex->getMessage());
            }

            return new Response($this->get('jms_serializer')->serialize($msg, 'json'));

        }

This is utility class that fetch error from form

class Utility {




static function getErrorMessages(\Symfony\Component\Form\Form $form) {
    $errors = array();
    foreach ($form->getErrors() as $key => $error) {
        $template = $error->getMessageTemplate();
        $parameters = $error->getMessageParameters();

        foreach($parameters as $var => $value){
            $template = str_replace($var, $value, $template);
        }

        $errors[$key] = $template;
    }
    //if ($form->hasChildren()) {
        foreach ($form->all() as $child) {
            if (!$child->isValid()) {
                $errors[$child->getName()] =  Utility::getErrorMessages($child);
            }
        }
    //}
    return $errors;
}



}

Form Class

class EpisodioForm  extends AbstractType {


    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('numeroEpisodio', 'text');
        $builder->add('titoloItaliano', 'text',array());
        $builder->add('titoloOriginale', 'text');
        $builder->add('dataIta', 'date', array('widget' => 'single_text', 'format' => 'dd/MM/yyyy'));
        $builder->add('dataJap', 'date', array('widget' => 'single_text', 'format' => 'dd/MM/yyyy'));
        $builder->add('durata', 'text');
        $builder->add('trama', 'textarea');
        $builder->add('extra', 'text');
        $builder->add('fansub', 'checkbox');
    }

    /**
     * Returns the name of this type.
     *
     * @return string The name of this type
     */
    public function getName()
    {
        return "episodio_form";
    }
}

The version of framework is Symfony 2.4

1
Could you add the code for the form to the question?Genti Saliu
I meant adding the code of the form class, the one which inherits from AbstractType.Genti Saliu
I tested Utility::getErrorMessages with a form of mine and it works as expected. Could you post the form class completely? It seems to have been cut off after the getName method.Genti Saliu
What is your Symfony version? Can you post you getErrorMessages() output?sintetico82
My Symfony version is 2.5.0-BETA2. It works the same with the BETA1 as well. This is the output I get: gist.github.com/gentisaliu/594aef7eb9259695c288Genti Saliu

1 Answers

2
votes

Why $this->context->addViolation($constraint->message); VS $this->context->addViolation($constraint->message, array('{{ value }}' => $value));?

This is because the date constraint validator shows the value which failed to validate in the error message, e.g.

45.04.2014 is not a valid date.

whereas the NotBlank constraint validator doesn't need to, since the value causing the error is always empty.

Putting the property name in the error message

This could be achieved:

  • by changing the annotations for the constraints in your entity to something like:

    class Episodio {
      /**
       *  @Assert\NotBlank(message="'titolo_italiano' should not be blank.")
       */
       protected $titolo_italiano;
    
       /**
        * @Assert\NotBlank(message="'titolo_originale' should not be blank.")
        */
        protected $titolo_originale;
    }
    

    I left out the column definitions and the other fields in the entity for the sake of readability.

  • OR by defining a custom validator which passes the name of the property to the error message:

    The constraint:

    class MyCustomNotBlank extends NotBlank 
    {
        public $message = "'{{ propertyName }}' should not be blank.";
    }
    

    The validator:

    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    
    class MyCustomNotBlankValidator extends ConstraintValidator
    {
        public function validate($value, Constraint $constraint)
        {
            if (!$value) {
                $this->context->addViolation(
                    $constraint->message,
                    array('{{ propertyName}}' => $this->context->getPropertyPath())
                );
            }
        }
    }
    

    This shows you how to define a custom validation constraint: http://symfony.com/doc/current/cookbook/validation/custom_constraint.html

Obtaining a key-value array with property name and error message

Afaik, there is no function in Symfony2's form component which does that, so it has to be implemented.

See https://stackoverflow.com/a/13763053/277106 . You can add that function to a service for reusability. In your (AJAX) controller you can then do:

public function someAction(Request $request)
{
    $errors = array();
    $form = $this->createForm(new MyType);

    if (!$form->bindRequest($request)->isValid()) {
        $errors = $this->myFormService->getErrorMessages($form);
    }
    else {
        // save the entity
    }
    return JsonResponse($errors);
}