简体   繁体   中英

Validation in Silex forms

I face a problem I don't manage to solve on my own concerning embedded forms.

I'm using Silex 1.3 with composer, the composer.json file is copied below. I'm not using Doctrine, and i made my own DAOs, thus I'm not using annotations.

I think my problem comes from my validation, or my data mapping here.

Context : I'm trying to work with the following objects :

  • Region (like Europe, NorthAmerica, etc.),
  • Country (France, Canada, etc.) which belong to a Region (and thus have it as attribute)
  • State (Ile de France, Quebec), which belong to a Country (and thus have it as attribute)

My aim is to use what I call SelectType , which are basically a form that allow me to select step by step some object, wihtout having to select directly into a huge list. The forms have the same logic as the objects, I have:

  • RegionType , which allow me to edit or add a Region ,
  • RegionSelectType , which allow me to select an existing Region ,
  • CountryType , which uses a RegionSelectType ,
  • CountrySelectType , which uses RegionSelectType which allow me to select a Region , then a Country in the selected Region ,
  • StateType , which uses a CountrySelectType
  • in a short future, I'll have a StateSelectType based on the same principle

When I try to submit my form ( StateType ), either by ajax or manually, the $form->isSumbitted()&&$form->isValid() returns true , with the Region filled, but without the Country filled (which is obvious since I did not select it).

Am I doing something wrong with my forms ?

I noticed that everything was going fine when I was not using my SelectType , but when I was populating the form options manually for each form (which caused a lot of code to be reccurent). The form was properly validated then.

Thank you for your time and help !

Composer.json:

{
    "require": {
        "silex/silex": "~1.3",
        "doctrine/dbal": "2.5.*",
        "symfony/security": "2.7.*",
        "twig/twig": "1.21.*",
        "symfony/twig-bridge": "2.7.*",
        "symfony/form": "2.7.*",
        "symfony/translation": "2.7.*",
        "symfony/config": "2.7.*",
        "jasongrimes/silex-simpleuser": "*",
        "twig/extensions": "1.3.*",
        "symfony/validator": "2.*",
        "phpoffice/phpexcel": "1.*",
        "symfony/monolog-bridge": "*"
    },
    "require-dev": {
        "phpunit/phpunit": "*",
        "symfony/browser-kit": "*",
        "symfony/css-selector": "*",
        "silex/web-profiler": "*"
    },
    "autoload":{
        "psr-4":{"Easytrip2\\": "src"}
    },
    "autoload-dev":{
        "psr-4":{"Easytrip2\\": "tests"}
    }
}

The StateController which does the form management:

public function stateAddAction(Request $request, Application $app) {
    $formView = null;
    if ($app ['security.authorization_checker']->isGranted ( 'ROLE_ADMIN' ) and $app ['security.authorization_checker']->isGranted ( 'ROLE_ADMIN' )) {
        // A user is fully authenticated : he can add comments
        $new = new State ();
        $form = $app ['form.factory']->create ( new StateType ( $app ), $new );
        $form->handleRequest ( $request );
        //this returns true, event if the country is not filled.
        if ($form->isSubmitted () && $form->isValid ()) {
            if ($app ['dao.state']->save ( $new )) {
                $app ['session']->getFlashBag ()->add ( 'success', 'Succesfully added.' );
                return $app->redirect ( $app ['url_generator']->generate ( 'state' ) );
            } else {
                $app ['session']->getFlashBag ()->add ( 'error', 'Error in SQL ! Not added...' );
            }
        }
        $formView = $form->createView ();

        return $app ['twig']->render ( 'form.html.twig', array (
                'title' => 'Add state',
                'scripts_ids' => StateType::getRefNames (),
                'form' => $formView
        ) );
    } else {
        $app ['session']->getFlashBag ()->add ( 'error', 'Don\'t have the rights...' );
        return $app->redirect ( $app ['url_generator']->generate ( 'home' ) );
    }
}

The AbstractEasytrip2Type , which is basically the injection of the app to be able to use the DAOs:

<?php

namespace Easytrip2\Form;

use Silex\Application;
use Symfony\Component\Form\AbstractType;

abstract class AbstractEasytrip2Type extends AbstractType {
    /**
     *
     * @var Application
     */
    protected $app;
    public function __construct(Application $app/*, $data*/) {
        $this->app = $app;
    }
    public static function getRefNames() {
        return null;
    }
}

The RegionSelectType :

<?php

namespace Easytrip2\Form\Select;

use Easytrip2\Form\AbstractEasytrip2Type;
use Easytrip2\Form\Select\DataMapper\RegionSelectDataMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class RegionSelectType extends AbstractEasytrip2Type {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $obj = $this->app ['dao.region']->findAll ();
        $builder->add ( 'choice', 'choice', array (
                'choices' => $obj,
                'choices_as_values' => true,
                'choice_label' => function ($value) {
                    // if nothing exists, then an empty label is generated.
                    return is_null ( $value ) ? "" : $value->getName ();
                },
                'choice_value' => function ($value) {
                    // here i only have int unsigned in database, so -1 is safe. This is probably used for comparison for selecting the stored object between the list and the stored object.
                    return is_null ( $value ) ? - 1 : $value->getId ();
                },
                'placeholder' => 'Select a region',
                'label' => 'Region'
        ) );
        $builder->setDataMapper ( new RegionSelectDataMapper () );
    }
    /**
     *
     * {@inheritDoc}
     *
     * @see \Symfony\Component\Form\AbstractType::setDefaultOptions()
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults ( array (
                'data_class' => 'Easytrip2\Domain\Region',
                'cascade_validation' => true
        ) );
    }
    public function getName() {
        return 'region';
    }
    public static function getRefNames() {
        return array ();
    }
}

The RegionSelectDataMapper:

<?php

namespace Easytrip2\Form\Select\DataMapper;

use Symfony\Component\Form\DataMapperInterface;

class RegionSelectDataMapper implements DataMapperInterface {
    public function mapDataToForms($data, $forms) {
        $forms = iterator_to_array ( $forms );
        $forms ['choice']->setData ( $data );
    }
    public function mapFormsToData($forms, &$data) {
        $forms = iterator_to_array ( $forms );
        $data = $forms ['choice']->getData ();
    }
}

The CountrySelectType:

<?php

namespace Easytrip2\Form\Select;

use Easytrip2\Domain\Region;
use Easytrip2\Form\AbstractEasytrip2Type;
use Easytrip2\Form\Select\DataMapper\CountrySelectDataMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class CountrySelectType extends AbstractEasytrip2Type {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add ( 'region', new RegionSelectType ( $this->app ), array (
                'label' => false,
                'cascade_validation' => true
        ) );
        $builder->addEventListener ( FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $this->modifyFormFromRegion ( $event->getForm (), $event->getData () ? $event->getData ()->getRegion () : null );
        } );
        $builder->get ( 'region' )->addEventListener ( FormEvents::POST_SUBMIT, function (FormEvent $event) {
            $this->modifyFormFromRegion ( $event->getForm ()->getParent (), $event->getForm ()->getData () );
        } );
        $builder->setDataMapper ( new CountrySelectDataMapper () );
    }
    public function modifyFormFromRegion(FormInterface $builder, Region $data = null) {
        $obj = array ();
        if (! is_null ( $data )) {
            $obj = $this->app ['dao.country']->findByRegionId ( $data->getId () );
        } else {
            // change this if you do not want the country to be filled with all countries.
            // $obj = $this->app ['dao.country']->findAll ();
            $obj = array ();
        }
        $builder->add ( 'choice', 'choice', array (
                'choices' => $obj,
                'choices_as_values' => true,
                'choice_label' => function ($value) {
                    // if nothing exists, then an empty label is generated.
                    return is_null ( $value ) ? "" : $value->getName ();
                },
                'choice_value' => function ($value) {
                    // here i only have int unsigned in database, so -1 is safe. This is probably used for comparison for selecting the stored object between the list and the stored object.
                    return is_null ( $value ) ? - 1 : $value->getId ();
                },
                'placeholder' => 'Select a country',
                'label' => 'Country',
                'required' => true,
                'data_class' => 'Easytrip2\Domain\Country',
                'cascade_validation' => true
        ) );
    }
    /**
     *
     * {@inheritDoc}
     *
     * @see \Symfony\Component\Form\AbstractType::setDefaultOptions()
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults ( array (
                'data_class' => 'Easytrip2\Domain\Country',
                'cascade_validation' => true
        ) );
    }
    function getName() {
        return 'country';
    }
    public static function getRefNames() {
        $ret = array (
                'in' => 'country_region_choice',
                'out' => 'country_choice'
        );
        return array (
                $ret
        );
    }
}

The CountrySelectDataMapper:

<?php

namespace Easytrip2\Form\Select\DataMapper;

use Symfony\Component\Form\DataMapperInterface;

class CountrySelectDataMapper implements DataMapperInterface {
    public function mapDataToForms($data, $forms) {
        $forms = iterator_to_array ( $forms );
        $forms ['choice']->setData ( $data );
        if (isset ( $forms ['region'] )) {
            if ($data) {
                $forms ['region']->setData ( $data->getRegion () );
            }
        }
    }
    public function mapFormsToData($forms, &$data) {
        $forms = iterator_to_array ( $forms );
        $data = $forms ['choice']->getData ();
    //  $data->getRegion() === $forms['']
    }
}

The StateType:

<?php

namespace Easytrip2\Form\Type;

use Easytrip2\Form\AbstractEasytrip2Type;
use Easytrip2\Form\Select\CountrySelectType;
use Easytrip2\Form\Select\GeopointSelectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class StateType extends AbstractEasytrip2Type {

    /**
     *
     * {@inheritDoc}
     *
     * @see \Symfony\Component\Form\AbstractType::buildForm()
     */
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add ( 'name', 'text', array (
                'label' => 'State name'
        ) );
        $builder->add ( 'code', 'text', array (
                'label' => 'State code'
        ) );
        $builder->add ( 'unloc', 'text', array (
                'label' => 'State unloc code'
        ) );
        // TODO : the validation on this form appears to not be done, thus i try to save (as it is considered as valid) a object which is null, thus fail in the setters.
        $builder->add ( 'country', new CountrySelectType ( $this->app ), array (
                'label' => false,
                'cascade_validation' => true
        ) );
    /**
     * $builder->add ( 'hub', new GeopointSelectType ( $this->app, 'HUB' ), array (
     * 'label' => 'Select a hub if necessary'
     * ) );
     */
    }
    public static function getRefNames() {
        $return = array ();
        $countries = CountrySelectType::getRefNames ();
        // $hubs = GeopointSelectType::getRefNames ();
        $last;
        foreach ( $countries as $value ) {
            $return [] = array (
                    'in' => 'state_' . $value ['in'],
                    'out' => 'state_' . $value ['out']
            );
        }
        /*
         * foreach ( $hubs as $value ) {
         * $return [] = array (
         * 'in' => 'state_' . $value ['in'],
         * 'out' => 'state_' . $value ['out']
         * );
         * }
         */

        return $return;
    }

    /**
     *
     * {@inheritDoc}
     *
     * @see \Symfony\Component\Form\AbstractType::configureOptions()
     */
    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults ( array (
                'data_class' => 'Easytrip2\Domain\State'
        ) );
    }

    /**
     *
     * {@inheritDoc}
     *
     * @see \Symfony\Component\Form\FormTypeInterface::getName()
     */
    public function getName() {
        return 'state';
    }
}

I managed to do it using loadValidatorMetadata in my classes, and using some workaround. That way, Silex do the validation, and even send it for the browser to basically validate data.

I also simplified a lot the mappers.

Please feel free to ask me if you have any question regarding this project.

EDIT : Since, i have :

  • changed the DataMappers of the select forms
  • added the cascade_validation and carefully filled the data_class options to my forms, using setDefaultOptions
  • used the loadValidatorMetadata , I'm not sure it did something (maybe it allows to check if a particular selection is valid, triggered by the cascade_validation ?

It has been quite a lot of time since i fixed it, so i might forgot some things here.

But basically, what I understand is that my validation was not taking place properly, and that validating data in entities, mapping right data to entities and cascading validation to ensure that attributes gets validated as well was necessary.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM