Blog

Symfony2 Form and Validation without Classes

By Ryan Weaver − 27 September 2011
In the Development category

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 Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

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 Symfony\Component\Validator\Constraints\Email;
// ...

$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 Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\Url;
use Symfony\Component\Validator\Constraints\Date;
use Symfony\Component\Validator\Constraints\Collection;
// ...

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 Symfony\Component\Validator\Constraints\All;
// ...

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!

  • 2012-02-14 Guest
    thank you for article! but what should I do if one of the form fields should have more than one constraint?
  • 2012-01-09 Massimiliano Arione
    I tried this approach, but I don't get my fields filled with choiced values after submitting.
    Edit: never mind. I was calling createView() too early :-(
  • 2011-11-14 weaverryan
    Hey!

    I don't quite understand your question. It sounds like your question about how to enforce a validation constraint without an entity/form model is exactly what's covered at the very last part of this article.

    But if I'm missing your question, let me know!

    Thanks!
  • 2011-11-11 Silence
    hi everyone! I know it is not related to the topic, but, if I have a form and I want to create a validator constraint without creating a entity or form model, what can I do? Is it a good practice anyway? I'm finding that Symfony documentation does not say too much about it while plugins like FOS has Form Models and Handlers
  • 2011-10-17 Richard Hodgson
    Thank you, thank you, thank you! I've been working for hours on something and your post really helped!
  • 2011-10-12 RedruM
    Hello,

    thanks for this tutorial it was very helpful. but i have some difficulties in adding 'validation_constraint' in getDefaultOptions, when i do it i have this error :
    Expected argument of type array or Traversable and ArrayAccess, object given
    Thanks
  • 2011-10-10 xeonist
    Hi Ryan, 
    Thx for your reply... I got what i wanted.
    and BTW: superb explanation!!!!

    a question though: when creating the class that will embrace the entity along with some non-persisting attributes, i had to manually write setters/getters, since the only way i know to generate setters/getters is
    php app/console doctrine:generate:entities

    and my class is not an entity to saved in DB...

    thx again !!
  • 2011-10-07 weaverryan
    Hi there!

    So, your situation is not a-typical: it's a combination of a form with true entity fields, and other fields that are *not* part of an entity. For the overall strategy, see this link: http://symfony.com/doc/current.... The example is for MongoDB, but you can ignore that - the key here is how you setup your classes and forms.

    So, overall, you'll have your core entity class (e.g. User), which you apply validation to normally. You'll then create another regular PHP class (e.g. Registration) with the extra fields, and a field (e.g. user) where you set an instance of your entity object (e.g. an instance of User). You then create a form class for your new class (e.g. RegistrationType) and embed the entity form class (e.g. UserType) into it (see: http://symfony.com/doc/current.... Optionally, you can place constraints on your Registration class for the extra fields.

    When your form is submitted, the constraints on both classes are run, even though the top class doesn't persist to the database. You can grab your populated entity object like this, and then persist it like normal.

        $form->getData()->getUser();

    Now, I think you may have some more specific questions - but hopefully this will get you started. Reply if with other question!

    Thanks!
  • 2011-10-07 xeonist
    hey Guys,

    question: i have a form related to an entity that needs to be persisted.
    But what i wanna show the user is a screen containing all those persisted properties, along with other non-persisted fields.
    So when i submit my form, i get "
    Only field names mapped by Doctrine can be validated for uniqueness."my question is that: 
    - i need the persisted to get validated and persisted .
    - and i need to persist the other properties that are in the same form (non-persisted).

    any help ???
  • 2011-09-29 weaverryan
    Yes, Symfony calls the `getDefaultOptions` while it's building the form. So, overriding `getDefaultOptions` and adding something to it is exactly the same as passing an array of options to the `createFormBuilder` method. In both cases, you want to specify options for your form, and if you actually have a form class, `getDefaultOptions` lets you put the "default" options (including your new `validation_constraint`) right inside your form type class.

    Good luck!
  • 2011-09-28 weaverryan
    Yes, absolutely - that's something I failed to point out - once you've added the `validation_constraint` to your form, `isValid()` will work exactly like you'd expect.

    Thanks!
  • 2011-09-28 Dieter Van der Stock
    Thanks for the very helpful article! Perfect timing too, as I was just stuck on this :-)


    Question: what do you mean with your very last sentence?
    I am indeed using a form type class, because I want to re-use my 'custom form'. So I can indeed override the getDefaultOptions method. But how does that make it so that my validation code can be included in the formType class? Does Symfony call that method at some point in the form lifetime?
    I hope I'm making sense. To ask it in a short manner: I don't quite get what you mean with the last sentence ;-)
  • 2011-09-27 Gregoire Pineau
    And, of course, you can call isValid on your form.
    (See : https://github.com/lyrixx/Sile...