Symfony2 Form and Validation without Classes

Publié le

26/09/2011

How2Tips

Sep 27, 2011 − Ever wanted to use Symfony's validation system just to validate some value? How about using a form without a class behind it? In this post, we explore all the cool ways you can use forms and validation directly.

Ever wanted to use Symfony's validation system just to validate some value? Though not previously well-documented (my bad), it's really really simple!

Suppose we're creating a simple JSON endpoint: our frontend sends us an email address via AJAX, and if it's valid, we do something. Here's the setup:

<?php
namespace AcmeDemoBundleController;

use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;

class DefaultController extends Controller
{
    public function updateAction(Request $request)
    {
        $email = $request->request->get('email');
        if (!$email) {
            throw $this->createNotFoundException('But where's the email???');
        }

        // we'll fill this next part in momentarily
        if (false) {
            $data = array('success' => true);
        } else {
            $data = array('success' => false, 'error' => '');
        }

        return new Response(json_encode($data));
    }
}

Notice I'm using the Request object as an argument to my Controller. This is another little-known feature, and is possible if you type-hint any controller argument with the Request class (don't forget your use statement!)

Now, let's validate that the email POST variable is a legitimate email address:

<?php
// ...

// add this above your class
use SymfonyComponentValidatorConstraintsEmail;
// ...

$emailConstraint = new Email();
$emailConstraint->message = 'Invalid email address';

$errorList = $this->get('validator')->validateValue($email, $emailConstraint);
if (count($errorList) == 0) {
    $data = array('success' => true);
} else {
    $data = array('success' => false, 'error' => $errorList[0]->getMessage());
}

The key is to use the validateValue method on the validator service. With this method, you can instantiate and pass any validation constraint (full list of validation constraints).

The validateValue method returns a ConstraintViolationList object, which is the scariest name we could think of to mean "an array of errors". Each item in this array-like object is a ConstraintViolation, which holds a message about the error. We can use the first error to return a message back to our frontend. Pretty neat, huh?

Validating an array of data

And what if you want to validate an array of data instead? Let's suppose that our JSON endpoint receives an array of data: an email address, a URL, and a date string. We could create three constraints and call validateValue on each individual piece of data. Actually, that's a great solution! But, because I'm eventually going to show you how to pass custom validation constraints to a form, let's to this all at once with the Collection constraint:

<?php

// add these atop your class
use SymfonyComponentValidatorConstraintsEmail;
use SymfonyComponentValidatorConstraintsUrl;
use SymfonyComponentValidatorConstraintsDate;
use SymfonyComponentValidatorConstraintsCollection;
// ...

public function updateAction(Request $request)
{
    // contains an email, url and date key
    $data = $request->request->get('data');

    // create a collection of constraints
    $collectionConstraint = new Collection(array(
        'email' => new Email(array('message' => 'Invalid email address')),
        'url'   => new Url(),
        'date'  => new Date(),
    ));

    $errorList = $this->get('validator')->validateValue($data, $collectionConstraint);

    if (count($errorList) == 0) {
        $data = array('success' => true);
    } else {
        $errors = array();
        foreach ($errorList as $error) {
            // getPropertyPath returns form [email], so we strip it
            $field = substr($error->getPropertyPath(), 1, -1);

            $errors[$field] = $error->getMessage();
        }

        $data = array('success' => false, 'errors' => $errors);
    }

    return new Response(json_encode($data));
}

This is pretty cool, but admittedly a bit messy, since the errors list that you get back needs to be massaged in order to assign each to the right piece of data. What's much cooler is how this strategy can easily be applied to validate forms that don't have a class behind them (see below).

Also, if you have an array of all of the same type - like an array of email addresses - you should check out the All constraint. This handy constraint will validate each element in an array against a certain constraint:

<?php

// add this above your class
use SymfonyComponentValidatorConstraintsAll;
// ...

public function updateAction(Request $request)
{
    // array of emails: e.g. emails[]=foo@bar.com
    $emails = $request->request->get('emails');

    $all = new All(array(new Email()));
    $errorList = $this->get('validator')->validateValue($emails, $all);

    if (count($errorList) == 0) {
        $data = array('success' => true);
    } else {
        $badEmails = array();
        foreach ($errorList as $error) {
            $badEmails = $error->getInvalidValue();
        }

        $data = array('success' => false, 'badEmails' => $badEmails);
    }

    return new Response(json_encode($data));
}

Symfony Forms without Classes

If you've read Symfony's form documentation, then you probably think that all forms must have a class behind them. This is actually not true - you can simply create a form and bind data to it. If you do this, the form returns an array of data instead of an object:

<?php

public function updateAction(Request $request)
{
    $form = $this->createFormBuilder()
        ->add('email', 'email')
        ->add('siteUrl', 'url')
        ->getForm()
    ;

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

        // the data is an *array* containing email and siteUrl
        $data = $form->getData();

        // do something with the data
    }

    return $this->render('AcmeDemoBundle:Default:update.html.twig', array(
        'form' => $form->createView()
    ));
}

Woh! Awesome! But what about validation? Usually, when you call $form->isValid(), Symfony applies the validation found on the underlying class. But without a class, how do you add validation constraints?

To do this, just create a constraint and pass it as the validation_constraint option. Since a form is a collection of fields, we'll use the Collection constraint:

<?php
// don't forget the use statements at the top of the class - see earlier example

$collectionConstraint = new Collection(array(
    'email' => new Email(array('message' => 'Invalid email address')),
    'siteUrl'   => new Url(),
));

$form = $this->createFormBuilder(null, array(
    'validation_constraint' => $collectionConstraint,
))
    ->add('email', 'email')
    ->add('siteUrl', 'url')
    ->getForm()
;

// ...

That's it! The errors will correctly show up next to each field when you render your form. By manually passing in the validation constraints, we can have a fully-functional form without having a class behind it. If you're using a form type class (instead of building your the in your controller), override the getDefaultOptions method set the validation_constraint option there.

Happy forming!

Publié par

KNP
KNP Labs


Commentaires