2

How do you validate form posts with the MoneyType Field?

It's bad enough that it uses input type="text" instead of type="number", but worse it doesn't matter what's input, like "asdf", the response is always: valid form posted. How can I get useful error messages instructing the user that they need to put in a valid money amount, such as "43.21"?

I tried 'error_bubbling' => true on the add options, {{ form_errors(form) }} in the twig view, and $form->getErrors() in the controller as suggested by this answer, but those are always empty because no matter what, $form->isValid() always returns true regardless of user input.


Project Structure:

.
├── composer.json
├── composer.lock
├── pub
│   └── scratch.php
├── vendor
│   └── ...
└── views
    └── form.html.twig

scratch.php

<?php require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\Translator;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;

// the Twig file that holds all the default markup for rendering forms
// this file comes with TwigBridge
$defaultFormTheme = 'form_div_layout.html.twig';

$appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
$vendorTwigBridgeDir = dirname($appVariableReflection->getFileName());
// the path to your other templates
$viewsDir = realpath(__DIR__.'/../views');

$twig = new Twig_Environment(new Twig_Loader_Filesystem(array(
    $viewsDir,
    $vendorTwigBridgeDir.'/Resources/views/Form',
)));
$formEngine = new TwigRendererEngine(array($defaultFormTheme));

$twig->addExtension(
    new FormExtension(new TwigRenderer($formEngine))
);

$twig->addExtension(
    new TranslationExtension(new Translator('en'))
);

$formEngine->setEnvironment($twig);

// create your form factory as normal
$formFactory = Forms::createFormFactoryBuilder()
    ->addExtension(new HttpFoundationExtension())
    ->getFormFactory();

$formBuilder = $formFactory->createBuilder();

$formBuilder->add("amount", MoneyType::class, [
  'currency' => 'USD',
  'error_bubbling' => true
]);

$form = $formBuilder->getForm();

$request = Request::createFromGlobals();

$form->handleRequest($request);

if ($form->isValid()) {

    die('valid form posted.');

}

$form->getErrors(true);

echo $twig->render('form.html.twig', array(
    'form' => $form->createView(),
));

form.html.twig

{{ form_start(form) }}

    {{ form_errors(form) }}

    {{ form_widget(form) }}

    <input type="submit" />

{{ form_end(form) }}

composer.json

{
    "require": {
        "symfony/form": "^3.1",
        "symfony/twig-bridge": "^3.1",
        "symfony/translation": "^3.1",
        "symfony/http-foundation": "^3.1"
    }
}
Community
  • 1
  • 1
Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167
  • What's the purpose of your 'scratch.php' file? Why not instead use a Controller file? I'm just asking, since you have a lot of code in the 'scratch.php' file that doesn't seem necessary... and you also put it under the folder `pub`, which is not a Symfony standard. Maybe there is a reason why you are creating your project this way - can you explain? – Alvin Bunk Jul 05 '16 at 17:25
  • @AlvinBunk Standalone minimal, complete, verifiable example. I'm not using the framework, testing the waters with Symfony because the components are supposedly hailed to be modular. – Jeff Puckett Jul 05 '16 at 17:29
  • Why don't you simply use the NumberType field instead of the MoneyType for your use case ? – Alsatian Jul 05 '16 at 19:37
  • 1
    @Alsatian because I'm dealing with currency money, not just a number, and I'd like to take advantage of other properties of that class such as a wrapper for international currency exchanges - and the input type number is actually the core problem with my [linked question](http://stackoverflow.com/q/38190891/4233593). – Jeff Puckett Jul 05 '16 at 20:15

2 Answers2

0

You have to attach one ore more Constraints to your form field like this :

use Symfony\Component\Validator\Constraints\Regex;

$formBuilder->add("amount", MoneyType::class, [
  'currency' => 'USD',
  'error_bubbling' => true,
  'constraints' => [
    new Regex(array('pattern'=>'/\d+(\.\d+)?/','message'=>'must be numeric')),
  ]
]);

Then if the constraint is violated the message 'must be numeric' will be displayed and your form won't be valid.

Built in constraints are defined here : http://symfony.com/doc/current/reference/constraints.html

Validation usage is explained here : http://symfony.com/doc/current/book/forms.html#using-a-form-without-a-class

Alsatian
  • 3,086
  • 4
  • 25
  • 40
  • `Fatal error: Uncaught exception 'Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException' with message 'The option "constraints" does not exist.` – Jeff Puckett Jul 05 '16 at 17:07
  • Do you still have this error with the validator component ? – Alsatian Jul 05 '16 at 19:35
  • Thanks for checking back in, but there are actually lots of problems with this answer. 1) Your `use` statement has a typo, should be plural `Constraints` not singular `Constraint`. 2) It fails to catch alphabetic characters - maybe because the default MoneyType validator trumps it. 3) It fails on a valid amount, such as "12.34", plus it throws a PHP Warning `preg_match(): Delimiter must not be alphanumeric or backslash` – Jeff Puckett Jul 05 '16 at 20:12
  • You've fixed 1 & 3, but it's still not throwing the `'must be numeric'` message. It stops with the default `This value is not valid.` – Jeff Puckett Jul 05 '16 at 20:19
  • 'This value is not valid' is written in you {{ form_errors(form) }} block ? Strange – Alsatian Jul 05 '16 at 21:03
  • No, I removed that block. It's written in `{{ form_widget(form) }}` – Jeff Puckett Jul 05 '16 at 21:04
  • It wpuld be easier to uqe the full framework to have all debuf informations ... Can you reuse the two blocks and replace (form) with form.amount ? And here check that this message is provided vy the form_errors(form.amount) ? – Alsatian Jul 05 '16 at 21:09
  • I appreciate your interest, but I'm fine with the default validator now that I've got it working. You've got the full source code here if you want to try to reproduce your custom validation message. Edit your question if you get it working and ping me. – Jeff Puckett Jul 05 '16 at 21:19
0

I figured out that I was missing the Validator component.

use Symfony\Component\Validator\Validation;

// Set up the Validator component
$validator = Validation::createValidator();

Then I needed to add the validator extension to the form factory.

use Symfony\Component\Form\Extension\Validator\ValidatorExtension;

// create your form factory as normal
$formFactory = Forms::createFormFactoryBuilder()
    ...
    ->addExtension(new ValidatorExtension($validator))
    ...

This of course needed composer require symfony/validator

So, I did not need

  • $form->getErrors(true)
  • 'error_bubbling' => true
  • {{ form_errors(form) }}

scratch.php

<?php require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;

// the Twig file that holds all the default markup for rendering forms
// this file comes with TwigBridge
$defaultFormTheme = 'form_div_layout.html.twig';

$appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
$vendorTwigBridgeDir = dirname($appVariableReflection->getFileName());
// the path to your other templates
$viewsDir = realpath(__DIR__.'/../views');

$twig = new Twig_Environment(new Twig_Loader_Filesystem(array(
    $viewsDir,
    $vendorTwigBridgeDir.'/Resources/views/Form',
)));
$formEngine = new TwigRendererEngine(array($defaultFormTheme));

$twig->addExtension(
    new FormExtension(new TwigRenderer($formEngine))
);

$twig->addExtension(
    new TranslationExtension(new Translator('en'))
);

$formEngine->setEnvironment($twig);

// Set up the Validator component
$validator = Validation::createValidator();

// create your form factory as normal
$formFactory = Forms::createFormFactoryBuilder()
    ->addExtension(new HttpFoundationExtension())
    ->addExtension(new ValidatorExtension($validator))
    ->getFormFactory();

$formBuilder = $formFactory->createBuilder();

$formBuilder->add("amount", MoneyType::class, [
  'currency' => 'USD'
]);

$form = $formBuilder->getForm();

$request = Request::createFromGlobals();

$form->handleRequest($request);

if ($form->isValid()) {

    die('valid form posted.');

}

$form->getErrors(true);

echo $twig->render('form.html.twig', array(
    'form' => $form->createView(),
));

form.html.twig

{{ form_start(form) }}

    {{ form_widget(form) }}

    <input type="submit" />

{{ form_end(form) }}

composer.json

{
    "require": {
        "symfony/form": "^3.1",
        "symfony/twig-bridge": "^3.1",
        "symfony/translation": "^3.1",
        "symfony/http-foundation": "^3.1"
        "symfony/validator": "^3.1"
    }
}
Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167