Cómo crear una restricción de validación personalizada

Puedes crear una restricción personalizada extendiendo la clase base «constraint», Symfony\Component\Validator\Constraint. A modo de ejemplo vas a crear un sencillo validador que compruebe si una cadena únicamente contiene caracteres alfanuméricos.

Creando la clase de la restricción

En primer lugar necesitas crear una clase para la restricción que extienda de Symfony\Component\Validator\Constraint:

// src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumeric.php
namespace Acme\DemoBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class ContainsAlphanumeric extends Constraint
{
    public $message = 'The string "%string%" contains an illegal character: it can only contain letters or numbers.';
}

Nota

La anotación @Annotation es necesaria para esta nueva restricción con el fin de hacerla disponible para su uso en clases vía anotaciones. Las opciones para tu restricción se representan como propiedades públicas en la clase constraint.

Creando el validador directamente

Como puedes ver, una clase de restricción es bastante mínima. La validación real la lleva a cabo otra clase «validadora de restricción». La clase validadora de restricción se configura con el método validatedBy() de la restricción, el cual por omisión incluye alguna sencilla lógica:

// en la clase base Symfony\Component\Validator\Constraint
public function validatedBy()
{
    return get_class($this).'Validator';
}

En otras palabras, si creas una restricción personalizada (por ejemplo, MyConstraint), cuando Symfony2 realmente lleve a cabo la validación automáticamente buscará otra clase, MyConstraintValidator.

La clase validator también es simple, y sólo tiene un método obligatorio: validate:

// src/Acme/DemoBundle/Validator/Constraints/ContainsAlphanumericValidator.php
namespace Acme\DemoBundle\Validator\Constraints;

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

class ContainsAlphanumericValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!preg_match('/^[a-zA-Za0-9]+$/', $value, $matches)) {
            $this->context->addViolation($constraint->message, array('%string%' => $value));
        }
    }
}

Nota

El método validate no devuelve un valor; en cambio, añade violaciones al validador de la propiedad context con una llamada al método addViolation si hay errores de validación. Por lo tanto, un valor se podría considerar como válido si no provoca que se añadan violaciones al contexto. El primer parámetro de la llamada a addViolation es el mensaje de error a utilizar para esa violación.

Usando el nuevo Validador

Usar validadores personalizados es muy fácil, así como los proporcionados por el mismo Symfony2:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\DemoBundle\Entity\AcmeEntity:
        properties:
            name:
                - NotBlank: ~
                - Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~
    
  • Annotations
    // src/Acme/DemoBundle/Entity/AcmeEntity.php
    use Symfony\Component\Validator\Constraints as Assert;
    use Acme\DemoBundle\Validator\Constraints as AcmeAssert;
    
    class AcmeEntity
    {
        // ...
    
        /**
         * @Assert\NotBlank
         * @AcmeAssert\ContainsAlphanumeric
         */
        protected $name;
    
        // ...
    }
    
  • XML
    <!-- src/Acme/DemoBundle/Resources/config/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="Acme\DemoBundle\Entity\AcmeEntity">
            <property name="name">
                <constraint name="NotBlank" />
                <constraint name="Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric" />
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
    // src/Acme/DemoBundle/Entity/AcmeEntity.php
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric;
    
    class AcmeEntity
    {
        public $name;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new NotBlank());
            $metadata->addPropertyConstraint('name', new ContainsAlphanumeric());
        }
    }
    

Si tu restricción contiene opciones, entonces deberían ser propiedades públicas en la clase de la restricción personalizada que creaste anteriormente. Estas opciones se pueden configurar como opciones en las restricciones del núcleo de Symfony.

Validadores de restricción con dependencias

Si el validador de la restricción tiene dependencias, tal como una conexión a base de datos, la tendrás que configurar como un servicio en el contenedor de inyección de dependencias. Este servicio debe incluir la etiqueta validator.constraint_validator y un atributo alias:

  • YAML
    services:
        validator.unique.your_validator_name:
            class: Nombre\De\Clase\Validator\Completamente\Cualificado
            tags:
                    - { name: validator.constraint_validator, alias: alias_name }
    
  • XML
    <service id="validator.unique.your_validator_name" class="Nombre\De\Clase\Validator\Completamente\Cualificado">
        <argument type="service" id="doctrine.orm.default_entity_manager" />
        <tag name="validator.constraint_validator" alias="alias_name" />
    </service>
    
  • PHP
    $container
        ->register('validator.unique.your_validator_name', 'Fully\Qualified\Validator\Class\Name')
        ->addTag('validator.constraint_validator', array('alias' => 'alias_name'));
    

Tu clase restricción ahora debe usar este alias para hacer referencia al validador adecuado:

public function validatedBy()
{
    return 'alias_name';
}

Como mencionamos anteriormente, Symfony2 automáticamente buscará una clase llamada después de la restricción, con su Validador adjunto. Si tu restricción de validación está definida como un servicio, es importante que redefinas el método validatedBy() para que devuelva el alias utilizado cuando definiste tu servicio, de lo contrario Symfony2 no utilizará el servicio de la restricción de validación, y, en su lugar, creará una instancia de la clase, sin inyectar ningún tipo de dependencia.

Clase para la validación de restricción

Además de validar una propiedad de clase, una restricción puede tener un ámbito de clase proporcionándole un objetivo:

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

Con esto, el método validador validate() obtiene un objeto como primer argumento:

class ProtocolClassValidator extends ConstraintValidator
{
    public function validate($protocol, Constraint $constraint)
    {
        if ($protocol->getFoo() != $protocol->getBar()) {
            $this->context->addViolationAt('foo', $constraint->message, array(), null);
        }
    }
}

Ten en cuenta que una clase validadora de restricciones se aplica a la misma clase, y no a la propiedad:

  • YAML
    # src/Acme/BlogBundle/Resources/config/validation.yml
    Acme\DemoBundle\Entity\AcmeEntity:
        constraints:
            Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric: ~
    
  • Annotations
    /**
     * @AcmeAssert\ContainsAlphanumeric
     */
    class AcmeEntity
    {
        // ...
    }
    
  • XML
    <!-- src/Acme/BlogBundle/Resources/config/validation.xml -->
    <class name="Acme\DemoBundle\Entity\AcmeEntity">
        <constraint name="Acme\DemoBundle\Validator\Constraints\ContainsAlphanumeric" />
    </class>
    
Bifúrcame en GitHub