Skip to content

Validation Frontend (Symfony Constraints) ​

vue-form intègre un moteur de validation côté client puissant et autonome, conçu pour répliquer le comportement du composant Validator de Symfony. Ce moteur permet de valider les données saisies par l'utilisateur en temps réel, sans avoir à soumettre le formulaire au serveur pour les vérifications de base.


Sommaire ​


Fonctionnement Général ​

Le backend Symfony sérialise les contraintes appliquées aux champs dans un format JSON, qui est envoyé au frontend via la propriété form.vars.constraints.

Le moteur de validation front-end :

  1. Écoute les changements : Un composable Vue (useValidation) surveille les saisies de l'utilisateur avec un petit délai (debounce de 400ms) pour ne pas être trop intrusif, et réagit instantanément à la perte de focus (blur / focusout).
  2. Exécute les règles : Les contraintes JSON sont hydratées sous forme de classes TS et évaluées.
  3. Affiche les erreurs : Les erreurs frontend sont fusionnées avec les erreurs existantes du backend (provenant de form.vars.errors) et affichées via le composant <form-errors>.

Contraintes non évaluables (mode strict) ​

Une contrainte que le client ne sait pas évaluer ne donne jamais un faux succès. C'est le cas d'une contrainte sans type, d'un type non enregistré (ex. Iban) ou d'une contrainte qui lève une exception (ex. motif Regex PCRE non supporté par JavaScript comme (?i)). Par défaut, le champ reçoit alors une violation générique (This value could not be verified.), une seule par champ, et l'envoi est bloqué.

Pour laisser le serveur trancher, désactivez le mode strict. Les contraintes non évaluables sont alors ignorées et seul un message console est émis :

typescript
import { defaultValidator } from '@codifyo/form-vue/src/validation';

defaultValidator.setStrict(false);
// ou garder le mode strict avec un message traduit :
defaultValidator.setStrict(true, 'Cette valeur n\'a pas pu être vérifiée.');

Structure JSON (Côté Symfony) ​

Pour que la validation front-end fonctionne, le backend doit envoyer un tableau d'objets définissant le type de contrainte et ses options. La structure imite parfaitement les classes Symfony correspondantes.

json
"constraints": [
    { 
        "type": "NotBlank", 
        "message": "Veuillez renseigner ce champ." 
    },
    { 
        "type": "Length", 
        "min": 5, 
        "max": 50, 
        "minMessage": "Le texte doit faire au moins {{ limit }} caractères.",
        "maxMessage": "Le texte ne doit pas dépasser {{ limit }} caractères."
    }
]

Contraintes Supportées ​

La majorité des contraintes natives de Symfony sont supportées :

Basic ​

  • NotBlank / Blank
  • NotNull / IsNull
  • IsTrue / IsFalse

String ​

  • Length (options: min, max, minMessage, maxMessage, exactMessage)
  • Email
  • Regex (options: pattern, match)
  • Url (options: protocols, requireTld, tldMessage)

Number ​

  • Range (options: min, max, notInRangeMessage, minMessage, maxMessage)
  • Positive / PositiveOrZero
  • Negative

Comparison ​

Les contraintes de comparaison permettent d'évaluer une valeur par rapport à une valeur cible fixe (value) ou dynamiquement par rapport à un autre champ du formulaire (propertyPath) :

  • EqualTo / NotEqualTo
  • IdenticalTo / NotIdenticalTo
  • LessThan / LessThanOrEqual
  • GreaterThan / GreaterThanOrEqual

Elles prennent en charge :

  • Les nombres : comparaison arithmétique standard.
  • Les chaînes de caractères : comparaison lexicographique.
  • Les dates et datetimes : comparaison chronologique (supporte les instances Date, les chaînes YYYY-MM-DD, YYYY-MM-DD HH:mm:ss et ISO).

Il est possible de comparer la valeur d'un champ avec n'importe quel autre champ de l'arborescence de formulaire en utilisant la syntaxe native Symfony Form :

  • parent.all[autreChamp].data
  • parent.children[autreChamp].data
  • parent[autreChamp].data
  • Chaînage d'accès complexes : parent.all[adresse].all[ville].data ou parent.parent...

Lorsqu'un champ surveille un autre champ via propertyPath, sa validation se réexécute automatiquement et réactivement lorsque le champ cible est modifié.

Comparaison de dates & Parité stricte avec Symfony PHP ​

Le moteur de validation applique une parité stricte avec le comportement de Symfony PHP (AbstractComparisonValidator) :

  • Champs DateType (YYYY-MM-DD) : En Symfony PHP, DateType réinitialise systématiquement l'heure à minuit (!Y-m-d). Côté front, toute valeur de date pure YYYY-MM-DD est ainsi instanciée à 00:00:00.000 en fuseau horaire local.
  • Comparaison basée sur les timestamps exacts : Aucune troncature artificielle n'est appliquée, reproduisant exactement la comparaison d'objets \DateTimeInterface de PHP :
    • Deux dates au format YYYY-MM-DD ou YYYY-MM-DD 00:00:00 (ex: contrainte 'today') possèdent toutes les deux l'heure 00:00:00 et sont donc strictement égales. Une contrainte GreaterThan rejettera l'égalité (violation), tandis que GreaterThanOrEqual l'acceptera.
    • Si la valeur cible fournie par le backend contient une heure non nulle (ex: 14:30:00 provenant par inadvertance d'un new \DateTime()), cette heure est prise en compte dans la comparaison exactement comme le fait PHP ($date > $comparedDate). Bonne pratique Symfony : Pour contraindre un champ DateType par rapport à la date du jour, privilégiez 'today' plutôt qu'une instance new \DateTime() avec l'heure courante.

Date ​

  • Date (Format YYYY-MM-DD)
  • DateTime

File ​

  • File et Image (options: maxSize, mimeTypes, maxSizeMessage, mimeTypesMessage)
    • Remarque : maxSize accepte les chaînes au format Symfony (ex: "2M", "1024k", "2.5MiB") ou les octets bruts.

Collection ​

  • Choice (options: choices sous forme de tableau, multiple)
  • Count (options: min, max)
  • All (options: constraints tableau de contraintes appliquées à chaque élément)

La Contrainte Callback ​

La contrainte personnalisée Callback est extrêmement puissante, car elle permet d'exécuter n'importe quelle logique JavaScript à la volée.

Structure depuis Symfony :

json
{
    "type": "Callback",
    "callback": "if (value === 'interdit') { return 'Ce mot est banni.'; } return true;"
}

La fonction JavaScript reçoit deux arguments :

  • value : La valeur actuelle du champ.
  • context : Un objet ValidationContext contenant :
    • rootModel : L'état réactif complet du formulaire (v-model).
    • propertyPath : Le nom / chemin complet du champ.
    • isVisible : Un booléen indiquant si le champ est actuellement visible dans l'interface (selon data-condition et les conteneurs parents).
    • validator : L'instance du validateur (Validator), permettant d'exécuter d'autres contraintes à la demande.

Exemples d'utilisation de Callback :

1. Valider en fonction d'un autre champ :

javascript
"if (context.rootModel.age < 18 && value === 'alcool') return 'Interdit aux mineurs !'; return true;"

2. Vérifier la visibilité du champ :

javascript
"if (!context.isVisible) return true; return value ? true : 'Champ requis lorsqu\'il est affiché';"

3. Chaîner d'autres contraintes via context.validator :

javascript
"const violations = context.validator.validate(value, [{ type: 'NotBlank' }, { type: 'Email' }], context.rootModel, context.propertyPath); return violations.length > 0 ? violations.map(v => v.message) : true;"

Valeurs de retour acceptées par le callback :

  • true : La valeur est valide.
  • false : La valeur est invalide (un message générique s'affichera).
  • string : La valeur est invalide, la string est utilisée comme message d'erreur.
  • string[] : Renvoie plusieurs messages d'erreurs en même temps.

Champs Conditionnels et Validation ​

Lorsqu'un champ possède un attribut data-condition (voir la documentation dédiée) et qu'il est masqué :

  • Toutes les validations du champ sont ignorées (y compris NotBlank), et le champ ne bloque pas la soumission du formulaire.
  • Si le champ était en erreur avant d'être masqué, ses erreurs sont automatiquement effacées.
  • Dès que le champ redevient visible, les validations s'appliquent à nouveau normalement.

Validation Globale et Blocage d'Envoi ​

Par défaut, les champs se valident en temps réel (à la frappe et à la perte de focus). Cependant, si vous avez besoin de bloquer l'envoi d'un formulaire qui contient des erreurs, le composant <symfony-form> met à disposition une vérification globale.

1. Lors de la soumission (@submit) ​

L'événement @submit émet un payload contenant la propriété isValid. Le composant s'occupe de valider l'ensemble de ses champs avant d'émettre cet événement :

vue
<template>
  <symfony-form :form="form" v-model="model" @submit="handleSubmit" />
</template>

<script setup>
function handleSubmit({ value, event, isValid }) {
  if (!isValid) {
    // Le formulaire contient des erreurs ! 
    // L'envoi est bloqué côté front et les champs en erreur sont mis en surbrillance.
    return;
  }
  
  // Tout est valide, envoi à l'API...
}
</script>

2. Validation Manuelle via ref ​

Vous pouvez aussi forcer la validation globale à tout moment via la méthode exposée validate() :

vue
<template>
  <symfony-form ref="myFormRef" :form="form" v-model="model" />
  <button type="button" @click="checkAndNext">Étape suivante</button>
</template>

<script setup>
import { ref } from 'vue';

const myFormRef = ref(null);

function checkAndNext() {
  // force la validation de l'arbre et renvoie true/false
  const isValid = myFormRef.value.validate();
  
  if (isValid) {
    // passe à l'étape suivante...
  }
}
</script>

Enregistrer des Contraintes Personnalisées ​

Si tu as créé des contraintes PHP spécifiques à ton application métier (ex: ValidSiret), tu peux créer son équivalent TypeScript et l'enregistrer dans le validateur.

Cette opération peut se faire n'importe où dans ton application (par ex. main.ts).

1. Créer la classe de contrainte ​

typescript
import { Constraint, ConstraintViolation, ValidationContext } from '@codifyo/form-vue/src/validation';

export class ValidSiret extends Constraint {
    // La méthode validate doit retourner un tableau de ConstraintViolation
    validate(value: any, context: ValidationContext): ConstraintViolation[] {
        // Ignorer si vide (laisser NotBlank s'en charger)
        if (this.isEmpty(value)) return [];
        
        // Logique métier...
        const isSiretValid = /^[0-9]{14}$/.test(String(value));
        
        if (!isSiretValid) {
            // this.createViolation génère l'objet d'erreur et remplace {{ var }} si besoin
            return [
                this.createViolation(
                    this.options.message || 'Le SIRET doit contenir exactement 14 chiffres.', 
                    {}, 
                    context, 
                    value
                )
            ];
        }
        
        return [];
    }
}

2. Enregistrer la contrainte ​

typescript
import { defaultValidator } from '@codifyo/form-vue/src/validation';
import { ValidSiret } from './MesContraintes/ValidSiret';

defaultValidator.registerConstraint('ValidSiret', ValidSiret);

Désormais, si le composant reçoit :

json
{ "type": "ValidSiret", "message": "Numéro SIRET invalide !" }

La validation s'exécutera automatiquement avec ta classe locale !


Utilisation Autonome (Hors Formulaire) ​

Le moteur de validation n'est pas strictement couplé aux formulaires Vue. Tu peux l'utiliser pour valider n'importe quelle donnée :

typescript
import { defaultValidator } from '@codifyo/form-vue/src/validation';

const maDonnee = "test@";
const mesContraintes = [
    { type: "NotBlank" },
    { type: "Email", message: "Email mal formatté." }
];

const erreurs = defaultValidator.validate(maDonnee, mesContraintes);

if (erreurs.length > 0) {
    console.log(erreurs.map(e => e.message)); 
    // Affiche : ["Email mal formatté."]
}