Documentation

You are viewing the documentation for the 2.7.5 release in the 2.7.x series of releases. The latest stable release series is 3.0.x.

§Handling form submission

Before you start with Play forms, read the documentation on the Play enhancer. The Play enhancer generates accessors for fields in Java classes for you, so that you don’t have to generate them yourself. You may decide to use this as a convenience. All the examples below show manually writing accessors for your classes.

§Enabling/Disabling the forms module

By default, Play includes the Java forms module (play-java-forms) when enabling the PlayJava sbt plugin, so there is nothing to enable if you already have enablePlugins(PlayJava) on your project.

The forms module is also available in PlayImport as javaForms, which can be used with libraryDependencies += javaForms in your build.sbt.

Note: If you are not using forms, you can remove the forms dependency by using the PlayMinimalJava sbt plugin instead of PlayJava. This also allows you to remove several transitive dependencies only used by the forms module, including several Spring modules and the Hibernate validator.

§Defining a form

The play.data package contains several helpers to handle HTTP form data submission and validation. The easiest way to handle a form submission is to define a play.data.Form that wraps an existing class:

import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;

public class User {

  protected String email;
  protected String password;
  protected FilePart<TemporaryFile> profilePicture;

  public void setEmail(String email) {
    this.email = email;
  }

  public String getEmail() {
    return email;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getPassword() {
    return password;
  }

  public FilePart<TemporaryFile> getProfilePicture() {
    return profilePicture;
  }

  public void setProfilePicture(FilePart<TemporaryFile> pic) {
    this.profilePicture = pic;
  }
}

The above form defines an email and a password text field and a profilePicture file input field, meaning the corresponding HTML form has to be defined with the multipart/form-data encoding to be able to upload the file.
As you can see, by default, you have to define getter and setter methods so Play is able to access the Form fields. You can however also enable “direct field access” (for all forms) by setting play.forms.binding.directFieldAccess = true in conf/application.conf. In this mode Play will ignore the getter and setter methods and will try to directly access the fields:

import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;

public class User {

  public String email;
  public String password;
  public FilePart<TemporaryFile> profilePicture;
}

Note: When using “direct field access” and a field is not accessible to Play during form binding (e.g. if a field or the class containing the field is not defined as public) Play will try to make a field accessible via reflection by calling field.setAccessible(true) internally. Depending on the Java version (8+), JVM and the Security Manager settings that could cause warnings about illegal reflective access or, in the worst case, throw a SecurityException

To wrap a class you have to inject a play.data.FormFactory into your Controller which then allows you to create the form:

Form<User> userForm = formFactory.form(User.class);

Instead of enabling “direct field access” for all forms, you can enable it only for specific ones:

Form<User> userForm = formFactory.form(User.class).withDirectFieldAccess(true);

Note: The underlying binding is done using Spring data binder.

This form can generate a User result value from a HashMap<String,String> for the text data and from a Map<String, FilePart<?>> for the file data:

Map<String, String> textData = new HashMap<>();
textData.put("email", "[email protected]");
textData.put("password", "secret");

Map<String, FilePart<?>> files = new HashMap<>();
files.put("profilePicture", myProfilePicture);

User user = userForm.bind(lang, attrs, textData, files).get();

If you have a request available in the scope, you can bind directly from the request content:

User user = userForm.bindFromRequest(request).get();

§Defining constraints

You can define additional constraints that will be checked during the binding phase using JSR-380 (Bean Validation 2.0) annotations:

public class User {

  @Required protected String email;
  protected String password;

  public void setEmail(String email) {
    this.email = email;
  }

  public String getEmail() {
    return email;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getPassword() {
    return password;
  }
}

Tip: The play.data.validation.Constraints class contains several built-in validation annotations. All of these constraint annotations are defined as @Repeatable. This lets you, for example, reuse the same annotation on the same element several times but each time with different groups. For some constraints however it makes sense to let them repeat itself anyway, like @ValidateWith/@ValidatePayloadWith for example.

In the Advanced validation section further below you will learn how to handle concerns like cross field validation, partial form validation or how to make use of injected components (e.g. to access a database) during validation.

§Handling binding failure

Of course if you can define constraints, then you need to be able to handle the binding errors.

if (userForm.hasErrors()) {
  return badRequest(views.html.form.render(userForm));
} else {
  User user = userForm.get();
  return ok("Got user " + user);
}

Typically, as shown above, the form simply gets passed to a template. Global errors can be rendered in the following way:

@if(form.hasGlobalErrors) {
    <p class="error">
        @for(error <- form.globalErrors) {
            <p>@error.format(messages)</p>
        }
    </p>
}

Errors for a particular field can be rendered in the following manner with error.format:

@for(error <- form("email").errors) {
    <p>@error.format(messages)</p>
}

Note that error.format takes messages() as an argument – this is an play.18n.Messages instance defined in JavaI18N.

§Filling a form with initial default values

Sometimes you’ll want to fill a form with existing values, typically for editing:

userForm = userForm.fill(new User("[email protected]", "secret"));

Tip: Form objects are immutable - calls to methods like bind() and fill() will return a new object filled with the new data.

§Handling a form with dynamic fields

You can use a DynamicForm if you need to retrieve data from an html form with dynamic fields:

public Result hello(Http.Request request) {
  DynamicForm requestData = formFactory.form().bindFromRequest(request);
  String firstname = requestData.get("firstname");
  String lastname = requestData.get("lastname");
  return ok("Hello " + firstname + " " + lastname);
}

§Register a custom DataBinder

In case you want to define a mapping from a custom object to a form field string and vice versa you need to register a new Formatter for this object.
You can achieve this by registering a provider for Formatters which will do the proper initialization.
For an object like JavaTime’s LocalTime it could look like this:

import java.text.ParseException;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;

import java.time.LocalTime;

import play.data.format.Formatters;
import play.data.format.Formatters.SimpleFormatter;
import play.i18n.MessagesApi;

@Singleton
public class FormattersProvider implements Provider<Formatters> {

  private final MessagesApi messagesApi;

  @Inject
  public FormattersProvider(MessagesApi messagesApi) {
    this.messagesApi = messagesApi;
  }

  @Override
  public Formatters get() {
    Formatters formatters = new Formatters(messagesApi);

    formatters.register(
        LocalTime.class,
        new SimpleFormatter<LocalTime>() {

          private Pattern timePattern = Pattern.compile("([012]?\\d)(?:[\\s:\\._\\-]+([0-5]\\d))?");

          @Override
          public LocalTime parse(String input, Locale l) throws ParseException {
            Matcher m = timePattern.matcher(input);
            if (!m.find()) throw new ParseException("No valid Input", 0);
            int hour = Integer.valueOf(m.group(1));
            int min = m.group(2) == null ? 0 : Integer.valueOf(m.group(2));
            return LocalTime.of(hour, min);
          }

          @Override
          public String print(LocalTime localTime, Locale l) {
            return localTime.format(DateTimeFormatter.ofPattern("HH:mm"));
          }
        });

    return formatters;
  }
}

After defining the provider you have to bind it:

import com.google.inject.AbstractModule;

import play.data.format.Formatters;

public class FormattersModule extends AbstractModule {

  @Override
  protected void configure() {

    bind(Formatters.class).toProvider(FormattersProvider.class);
  }
}

Finally you have to disable Play’s default FormattersModule and instead enable your module in application.conf:

play.modules.enabled += "com.example.FormattersModule"
play.modules.disabled += "play.data.format.FormattersModule"

When the binding fails an array of errors keys is created, the first one defined in the messages file will be used. This array will generally contain:

["error.invalid.<fieldName>", "error.invalid.<type>", "error.invalid"]

The errors keys are created by Spring DefaultMessageCodesResolver, the root “typeMismatch” is replaced by “error.invalid”.

§Advanced validation

Play’s built-in validation module is using Hibernate Validator under the hood. This means we can take advantage of features defined in the JSR-380 (Bean Validation 2.0). The Hibernate Validator documentation can be found here.

§Cross field validation

To validate the state of an entire object we can make use of class-level constraints.
To free you from the burden of implementing your own class-level constraint(s), Play out-of-the-box already provides a generic implementation of such constraint which should cover at least the most common use cases.

Now let’s see how this works: To define an ad-hoc validation, all you need to do is annotate your form class with Play’s provided class-level constraint (@Validate) and implement the corresponding interface (in this case Validatable<String>) - which forces you to override a validate method:

import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;

@Validate
public class User implements Validatable<String> {

  @Constraints.Required protected String email;
  protected String password;

  @Override
  public String validate() {
    if (authenticate(email, password) == null) {
      // You could also return a key defined in conf/messages
      return "Invalid email or password";
    }
    return null;
  }

  // getters and setters

The message returned in the above example will become a global error. Errors are defined as play.data.validation.ValidationError.
Also be aware that in this example the validate method and the @Constraints.Required constraint will be called simultaneously - so the validate method will be called no matter if @Constraints.Required was successful or not (and vice versa). You will learn how to introduce an order later on.

As you can see the Validatable<T> interface takes a type parameter which determines the return type of the validate() method.
So depending if you want to be able to add a single global error, one error (which could be global as well) or multiple (maybe global) errors to a form via validate(), you have to use either a String, a ValidationError or a List<ValidationError> as type argument. Any other return types of the validate method will be ignored by Play.

If validation passes inside a validate() method you must return null or an empty List. Returning any other non-null value (including empty string) is treated as failed validation.

Returning a ValidationError object may be useful when you have additional validations for a specific field:

import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;

@Validate
public static class LoginForm implements Validatable<ValidationError> {

  // fields, getters, setters, etc.

  @Override
  public ValidationError validate() {
    if (authenticate(email, password) == null) {
      // Error will be displayed for the email field:
      return new ValidationError("email", "Invalid credentials");
    }
    return null;
  }
}

You can add multiple validation errors by returning List<ValidationError>. This can be used to add validation errors for a specific field, global errors or even a mix of these options:

import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import java.util.List;
import java.util.ArrayList;

@Validate
public static class SignUpForm implements Validatable<List<ValidationError>> {

  // fields, getters, setters, etc.

  @Override
  public List<ValidationError> validate() {
    final List<ValidationError> errors = new ArrayList<>();
    if (authenticate(email, password) == null) {
      // Add an error which will be displayed for the email field:
      errors.add(new ValidationError("email", "Access denied"));
      // Also add a global error:
      errors.add(new ValidationError("", "Form could not be submitted"));
    }
    return errors;
  }
}

As you can see, when using an empty string as the key of a ValidationError it becomes a global error.

One more thing: Instead of writing out error messages you can use message keys defined in conf/messages and pass arguments to them. When displaying the validation errors in a template the message keys and it’s arguments will be automatically resolved by Play:

// Global error without internationalization:
new ValidationError("", "Errors occurred. Please check your input!");
// Global error; "validationFailed" should be defined in `conf/messages` - taking two arguments:
new ValidationError("", "validationFailed", Arrays.asList(arg1, arg2));
// Error for the email field; "emailUsedAlready" should be defined in `conf/messages` - taking
// the email as argument:
new ValidationError("email", "emailUsedAlready", Arrays.asList(email));

§Partial form validation via groups

When a user submits a form there can be use cases where you don’t want to validate all constraints at once but just some of them. For example think about a UI wizard where in each step only a specified subset of constraints should get validated.

Or think about the sign-up and the login process of a web application. Usually for both processes you want the user to enter an email address and a password. So these processes would require almost the same forms, except for the sign-up process the user also has to enter a password confirmation. To make things more interesting let’s assume a user can also change his user data on a settings page when he is logged in already - which would need a third form.

Using three different forms for such a case isn’t really a good idea because you would use the same constraint annotations for most of the form fields anyway. What if you have defined a max-length constraint of 255 for a name field and then want to change it to a limit of just 100? You would have to change this for each form. As you can imagine this would be error prone in case you forget to update one of the forms.

Luckily we can simply group constraints:

import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import javax.validation.groups.Default;

@Validate(groups = {SignUpCheck.class})
public class PartialUserForm implements Validatable<ValidationError> {

  @Constraints.Required(groups = {Default.class, SignUpCheck.class, LoginCheck.class})
  @Constraints.Email(groups = {Default.class, SignUpCheck.class})
  private String email;

  @Constraints.Required private String firstName;

  @Constraints.Required private String lastName;

  @Constraints.Required(groups = {SignUpCheck.class, LoginCheck.class})
  private String password;

  @Constraints.Required(groups = {SignUpCheck.class})
  private String repeatPassword;

  @Override
  public ValidationError validate() {
    if (!checkPasswords(password, repeatPassword)) {
      return new ValidationError("repeatPassword", "Passwords do not match");
    }
    return null;
  }

  // getters and setters

The SignUpCheck and LoginCheck group are defined as two interfaces:

public interface SignUpCheck {}
public interface LoginCheck {}

For the sign-up process we simply pass the SignUpCheck group to the form(...) method:

Form<PartialUserForm> form =
    formFactory().form(PartialUserForm.class, SignUpCheck.class).bindFromRequest(request);

In this case the email address is required and has to be a valid email address, both the password and the password confirmation are required and the two passwords have to be equal (because of the @Validate annotation which calls the validate method). But we don’t care about the first name and last name - they can be empty or we could even exclude these input fields in the sign up page.

For the login process we just pass the LoginCheck group instead:

Form<PartialUserForm> form =
    formFactory().form(PartialUserForm.class, LoginCheck.class).bindFromRequest(request);

Now we only require the email address and the password to be entered - nothing more. We don’t even care about if the email is valid. You probably wouldn’t display any of the other form fields to the user because we don’t validate them anyway.

Imagine we also have a page where the user can change the user data (but not the password):

Form<PartialUserForm> form =
    formFactory().form(PartialUserForm.class, Default.class).bindFromRequest(request);

Which is exactly the same as:

Form<PartialUserForm> form =
    formFactory().form(PartialUserForm.class).bindFromRequest(request);

In this case following constraints will be validated: The email address is required and has to be valid plus the first name and last name are required as well - that is because if a constraint annotation doesn’t explicitly define a group then the Default group is used.
Be aware we don’t check any of the password constraints: Because they explicitly define a group attribute but don’t include the Default group they won’t be taken into account here.

As you can see in the last example, when only passing the group javax.validation.groups.Default you can omit it - because it’s the default anyway.
But as soon you pass any other group(s) you would also have to pass the Default group explicitly if you want any of it’s fields taken into account during the validation process.

Tip: You can pass as many groups as you like to the form(...) method (not just one). Just to be clear: These groups will then be validated all at once - not one after the other.

For advanced usage a group of constraints can include another group. You can do that using group inheritance.

§Defining the order of constraint groups

You can validate groups in sequences. This means groups will be validated one after another - but the next group will only be validated if the previous group was validated successfully before. (However right now it’s not possible to determine the order of how constraints will be validated within a group itself - this will be part of a future version of Bean Validation)

Based on the example above let’s define a group sequence:

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({Default.class, SignUpCheck.class, LoginCheck.class})
public interface OrderedChecks {}

Now we can use it:

Form<PartialUserForm> form =
    formFactory().form(PartialUserForm.class, OrderedChecks.class).bindFromRequest(request);

Using this group sequence will first validate all fields belonging to the Default group (which again also includes fields that haven’t defined a group at all). Only when all the fields belonging to the Default group pass validation successfully, the fields belonging to the SignUpCheck will be validated and so on.

Using a group sequence is especially a good practice when you have a validate method which queries a database or performs any other blocking action: It’s not really useful to execute the method at all if the validation fails at it’s basic level (email is not valid, number is a string, etc). In such a case you probably want the validate be called only after checking all other annotation-based constraints before and only if they pass. A user, for example, who signs up should enter a valid email address and only if it is valid a database lookup for the email address should be done afterwards.

§Passing payloads to validators

If needed, you can also pass a ValidationPayload object - which contains useful information sometimes needed for a validation process - to a validate method.
To pass such a payload just annotate your form with @ValidateWithPayload (instead of just @Validate) and implement ValidatableWithPayload (instead of just Validatable):

import java.util.Map;

import com.typesafe.config.Config;

import play.data.validation.Constraints.ValidatableWithPayload;
import play.data.validation.Constraints.ValidateWithPayload;
import play.data.validation.ValidationError;
import play.data.validation.ValidationPayload;

import play.i18n.Lang;
import play.i18n.Messages;

@ValidateWithPayload
public class ChangePasswordForm implements ValidatableWithPayload<ValidationError>
public static class ChangePasswordForm implements ValidatableWithPayload<ValidationError> {

  // fields, getters, setters, etc.

  @Override
  public ValidationError validate(ValidationPayload payload) {
    Lang lang = payload.getLang();
    Messages messages = payload.getMessages();
        Map<String, Object> ctxArgs = payload.getArgs();            //
    //         Map<String, Object> ctxArgs = payload.getArgs();
    TypedMap attrs = payload.getAttrs();
    Config config = payload.getConfig();
    // ...
  }
}

§Custom class-level constraints with DI support

Sometimes you need more sophisticated validation processes. E.g. when a user signs up you want to check if his email address already exists in the database and if so validation should fail.

Because constraints support both runtime Dependency Injection and , we can easily create our own custom (class-level) constraint which gets a Database object injected - which we can use later in the validation process. Of course you can also inject other components like MessagesApi, JPAApi, etc.

Note: You only need to create one class-level constraint for each cross concern. For example, the constraint we will create in this section is reusable and can be used for all validation processes where you need to access the database. The reason why Play doesn’t provide any generic class-level constraints with dependency injected components is because Play doesn’t know which components you might have enabled in your project.

First let’s set up the interface with the validate method we will implement in our form later. You can see the method gets passed a Database object (Checkout the database docs):

Without Payload
import play.db.Database;

public interface ValidatableWithDB<T> {
  public T validate(final Database db);
}
With Payload
import play.db.Database;

import play.data.validation.Constraints.ValidationPayload;

public interface ValidatableWithDB<T> {
  public T validate(final Database db, final ValidationPayload payload);
}

We also need the class-level annotation we put on our form class:

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Repeatable(ValidateWithDB.List.class)
@Constraint(validatedBy = ValidateWithDBValidator.class)
public @interface ValidateWithDB {
  String message() default "error.invalid";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  /** Defines several {@code @ValidateWithDB} annotations on the same element. */
  @Target({TYPE, ANNOTATION_TYPE})
  @Retention(RUNTIME)
  public @interface List {
    ValidateWithDB[] value();
  }
}

Finally this is how our constraint implementation looks like:

Without Payload
import javax.inject.Inject;
import javax.validation.ConstraintValidatorContext;

import play.data.validation.Constraints.PlayConstraintValidator;

import play.db.Database;

public class ValidateWithDBValidator
    implements PlayConstraintValidator<ValidateWithDB, ValidatableWithDB<?>> {

  private final Database db;

  @Inject
  public ValidateWithDBValidator(final Database db) {
    this.db = db;
  }

  @Override
  public void initialize(final ValidateWithDB constraintAnnotation) {}

  @Override
  public boolean isValid(
      final ValidatableWithDB<?> value,
      final ConstraintValidatorContext constraintValidatorContext) {
    return reportValidationStatus(value.validate(this.db), constraintValidatorContext);
  }
}
With Payload
import javax.inject.Inject;
import javax.validation.ConstraintValidatorContext;

import play.data.validation.Constraints.PlayConstraintValidator;
import play.data.validation.Constraints.PlayConstraintValidatorWithPayload;
import play.data.validation.Constraints.ValidationPayload;

import play.db.Database;

public class ValidateWithDBValidator
    implements PlayConstraintValidatorWithPayload<ValidateWithDB, ValidatableWithDB<?>> {

  private final Database db;

  @Inject
  public ValidateWithDBValidator(final Database db) {
    this.db = db;
  }

  @Override
  public void initialize(final ValidateWithDB constraintAnnotation) {}

  @Override
  public boolean isValid(
      final ValidatableWithDB<?> value,
      final ValidationPayload payload,
      final ConstraintValidatorContext constraintValidatorContext) {
    return reportValidationStatus(value.validate(this.db, payload), constraintValidatorContext);
  }
}

Note: Don’t get confused with ValidationPayload and ConstraintValidatorContext: The former class is provided by Play and is what you use in your day-to-day work when dealing with forms in Play. The latter class is defined by the Bean Validation specification and is used only internally in Play - with one exception: This class emerges when your write your own custom class-level constraints, where you only need to pass it on to the reportValidationStatus method however anyway.

As you can see we inject the Database object into the constraint’s constructor and use it later when calling validate. When using runtime Dependency Injection, Guice will automatically inject the Database object, but for compile-time Dependency Injection you need to do that by yourself:

import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.data.FormFactoryComponents;
import play.data.validation.MappedConstraintValidatorFactory;
import play.db.DBComponents;
import play.db.HikariCPComponents;
import play.filters.components.NoHttpFiltersComponents;
import play.routing.Router;

public class ValidateWithDBComponents extends BuiltInComponentsFromContext
    implements FormFactoryComponents, DBComponents, HikariCPComponents, NoHttpFiltersComponents {

  public ValidateWithDBComponents(ApplicationLoader.Context context) {
    super(context);
  }

  @Override
  public Router router() {
    return Router.empty();
  }

  @Override
  public MappedConstraintValidatorFactory constraintValidatorFactory() {
    return new MappedConstraintValidatorFactory()
        .addConstraintValidator(
            ValidateWithDBValidator.class, new ValidateWithDBValidator(database("default")));
  }
}

Note: you don’t need to create the database instance by yourself, it is already defined in the implemented interfaces.

This way, your validator will be available when necessary.

When writing your own class-level constraints you can pass following objects to the reportValidationStatus method: A ValidationError, a List<ValidationError> or a String (handled as global error). Any other objects will be ignored by Play.

Finally we can use our custom class-level constraint to validate a form:

Without Payload
import play.data.validation.Constraints;
import play.data.validation.ValidationError;
import play.db.Database;

@ValidateWithDB
public class DBAccessForm implements ValidatableWithDB<ValidationError> {

  @Constraints.Required @Constraints.Email private String email;

  @Constraints.Required private String firstName;

  @Constraints.Required private String lastName;

  @Constraints.Required private String password;

  @Constraints.Required private String repeatPassword;

  @Override
  public ValidationError validate(final Database db) {
    // Access the database to check if the email already exists
    if (User.byEmail(email, db) != null) {
      return new ValidationError("email", "This e-mail is already registered.");
    }
    return null;
  }

  // getters and setters
With Payload
import play.data.validation.Constraints;
import play.data.validation.ValidationError;
import play.data.validation.Constraints.ValidationPayload;
import play.db.Database;

@ValidateWithDB
public class DBAccessForm implements ValidatableWithDB<ValidationError> {

  @Constraints.Required @Constraints.Email private String email;

  @Constraints.Required private String firstName;

  @Constraints.Required private String lastName;

  @Constraints.Required private String password;

  @Constraints.Required private String repeatPassword;

  @Override
  public ValidationError validate(final Database db, final ValidationPayload payload) {
    // Access the database to check if the email already exists
    if (User.byEmail(email, db) != null) {
      return new ValidationError("email", "This e-mail is already registered.");
    }
    return null;
  }

  // getters and setters

Tip: You might have recognized that you could even implement multiple interfaces and therefore add multiple class-level constraint annotations on your form class. Via validation groups you could then just call the desired validate method(s) (or even multiple at once during one validation process).

Next: Using the form template helpers