Dynamically disable form in form events symfony - php

I have many entities, each of which has its own form type. Some of the entities implements FooBarInterface comprising method FooBarInterface::isEnabled();
I want to create a Form Extension, to be check data_class at all forms, and disable form if entity implements FooBarInterface and entity::isEnabled() return false.
<?php
namespace AppBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class MyExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$dataClass = $builder->getDataClass();
if ($dataClass) {
$reflection = new \ReflectionClass($dataClass);
if ($reflection->implementsInterface(FooBarInterface::class)) {
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $formEvent) {
$data = $formEvent->getData();
if ($data && !$data->isEnabled()) {
// todo this need disable all form with subforms
}
});
}
}
}
public function getExtendedType()
{
return 'form';
}
}
I have to make it through the $ builder-> addEventListener, because $ builder-> getData () do not always have the time to create a form. But after the creation of the form, I can not change her option disabled
How do I change the option disabled in the form?

I created Form Extension with method
public function finishView(FormView $view, FormInterface $form, array $options)
{
$dataClass = $form->getConfig()->getDataClass();
if ($dataClass) {
$reflection = new \ReflectionClass($dataClass);
if ($reflection->implementsInterface(FooBarInterface::class)) {
/** #var FooBarInterface$data */
$data = $form->getData();
if ($data && !$data ->isEnabled()) {
$this->recursiveDisableForm($view);
}
}
}
}
private function recursiveDisableForm(FormView $view)
{
$view->vars['disabled'] = true;
foreach ($view->children as $childView) {
$this->recursiveDisableForm($childView);
}
}
For disable changes if data is submitted I create Doctrine Event Subscriber with FormEvents::PRE_UPDATE event:
/**
* #param PreUpdateEventArgs $args
*/
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof DataStateInterface) {
return;
}
if (!$entity->isEnabled()) {
$args->getEntityManager()->getUnitOfWork()->clearEntityChangeSet(spl_object_hash($entity));
}
}

Related

Symfony reusable AJAX select / ChoiceType

I want to create a reusable AJAX-based select (select2) using Symfony form types and I've spent quite some time on it but can't get it to work like I want.
As far as I know you cannot override options of form fields after they have been added, so you have to re-add them with the new config. The Symfony docs also provide some examples on how to dynamically add or modify forms using events. https://symfony.com/doc/current/form/dynamic_form_modification.html
I've managed to create my AJAX based elements in the form and it's working but not completely reusable yet:
Form field extends Doctrine EntityType to have full support of data mappers etc
Form field is initialized with 'choices' => [], so Doctrine does not load any entities from db
Existing choices on edit is added during FormEvents::PRE_SET_DATA
Posted choices are added during FormEvents::PRE_SUBMIT
The current setup works but only in the parent form. I want to have my AjaxNodeType completely reusable so the parent form does not need to care about data handling and events.
Following the 2 examples, parent and child form. Here with event listeners in both, but of course they should only be in one.
Is it simply not possible in single element types to replace "yourself" in the parent form or am I doing something wrong? Is there any other way to dynamically change the choices?
Parent form
This works!
class MyResultElementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'fixedNode',
AjaxNodeType::class,
[
'label' => 'Fixed node',
'required' => false,
]
);
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData()->getFixedNode();
//this works here but not in child???
if ($fixedNode) {
$name = 'fixedNode';
$parentForm = $event->getForm();
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$fixedNode];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
$data = $event->getData()['fixedNode'];
if ($data) {
$name = 'fixedNode';
$parentForm = $event->getForm();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => MyResultElement::class,
'method' => 'POST',
]
);
}
}
Child form / single select This does NOT work. On POST the fixedNode field is not set to the form data-entity.
class AjaxNodeType extends AbstractType
{
/** #var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//this does not work here but in parent???
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData();
$name = $event->getForm()->getName();
$parentForm = $event->getForm()->getParent();
$options = $parentForm->get($name)->getConfig()->getOptions();
$newChoices = [$fixedNode];
// check if the choices already match, otherwise we'll end up in an endless loop ???
if ($options['choices'] !== $newChoices) {
$options['choices'] = $newChoices;
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
if ($event->getData()) {
$name = $event->getForm()->getName();
$data = $event->getData();
$parentForm = $event->getForm()->getParent();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, self::class, $options);
}
},
1000
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => Node::class,
// prevent doctrine from loading ALL nodes
'choices' => [],
]
);
}
public function getParent(): string
{
return EntityType::class;
}
}
Again, answering my own Questions :)
After spending some more time on it I got a working solution. And maybe it will be helpful for someone.
I've switched from EntityType to ChoiceType as it only made things more complicated and is not actually needed
multiple and single selects need different settings/workarrounds (like by_reference), see the line comments below
re-adding yourself to the parent form works, I don't know why it did not before...
beware of endless-loops when re-adding / re-submitting values
The main re-usable AjaxEntityType that does all the logic without reference to any specific entity:
<?php
namespace Tool\Form\SingleInputs;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AjaxEntityType extends AbstractType
{
/** #var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// shamelessly copied from DoctrineType - this is needed to support Doctrine Collections in multi-selects
if ($options['multiple'] && interface_exists(Collection::class)) {
$builder
->addEventSubscriber(new MergeDoctrineCollectionListener())
->addViewTransformer(new CollectionToArrayTransformer(), true);
}
// PRE_SET_DATA is the entrypoint on form creation where we need to populate existing choices
// we process current data and set it as choices so it will be rendered correctly
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) use($options) {
$data = $event->getData();
$hasData = ($options['multiple'] && count($data) > 0) || (!$options['multiple'] && $data !== null);
if ($hasData) {
$entityOrList = $event->getData();
$name = $event->getForm()->getName();
$parentForm = $event->getForm()->getParent();
$options = $parentForm->get($name)->getConfig()->getOptions();
// ONLY do this if the choices are empty, otherwise readding PRE_SUBMIT will not work because this is called again!
if(empty($options['choices'])) {
if($options['multiple']) {
$newChoices = [];
foreach ($entityOrList as $item) {
$newChoices[$item->getId()] = $item;
}
} else {
$newChoices = [$entityOrList->getId() => $entityOrList];
}
$options['choices'] = $newChoices;
$parentForm->add($name, self::class, $options);
}
}
},
1000
);
// PRE_SUBMIT is the entrypoint where we need to process the submitted values
// we have to add the POST-ed choices, otherwise this field won't be valid
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use($options) {
$entityIdOrList = $event->getData();
$entityClass = $options['class'];
// new choices constructed from POST
$newChoices = [];
if ($options['multiple']) {
foreach ($entityIdOrList as $id) {
if ($id) {
$newChoices[$id] = $this->entityManager->find($entityClass, $id);
}
}
} elseif ($entityIdOrList) {
$newChoices = [$entityIdOrList => $this->entityManager->find($entityClass, $entityIdOrList)];
}
$name = $event->getForm()->getName();
$parentform = $event->getForm()->getParent();
$currentChoices = $event->getForm()->getConfig()->getOptions()['choices'];
// if the user selected/posted new choices that have not been in the existing list, add them all
if ($newChoices && count(array_diff($newChoices, $currentChoices)) > 0) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = $newChoices;
// re-add ourselves to the parent form with updated / POST-ed options
$parentform->add($name, self::class, $options);
if(!$parentform->get($name)->isSubmitted()) {
// after re-adding we also need to re-submit ourselves
$parentform->get($name)->submit($entityIdOrList);
}
}
}, 1000);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'mapped' => true,
'choice_value' => 'id',
'choice_label' => 'selectLabel',
'choices' => [],
'attr' => [
'class' => 'select2-ajax',
],
'ajax_url' => null,
]
);
// AJAX endpoint that is select2 compatible
// https://select2.org/data-sources/ajax
$resolver->setRequired('ajax_url');
$resolver->setAllowedTypes('ajax_url', ['string']);
// entity class to process
$resolver->setRequired('class');
// by_reference needs to be true for single-selects, otherwise our entities will be cloned!
// by_reference needs to be false for multi-selects, otherwise the setters wont be called for doctrine collections!
$resolver->setDefault('by_reference', function (Options $options) {
return !$options['multiple'];
});
// adds the ajax_url as attribute
$resolver->setNormalizer('attr', function (Options $options, $value) {
$value['data-custom-ajax-url'] = $options['ajax_url'];
return $value;
});
}
public function getParent(): string
{
return ChoiceType::class;
}
}
Actual usage with specific entity and ajax endpoint:
<?php
namespace Tool\Form\SingleInputs;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Tool\Entities\User\Node;
class AjaxNodeType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => Node::class,
'ajax_url' => 's/ajax-select/nodes',
]
);
}
public function getParent(): string
{
return AjaxEntityType::class;
}
}

Symfony 2 Transformer on entity form type

I'm trying to create a new form type in Symfony 2. It is based on entity type, it uses select2 on frontend and I need the user to be able to select existing entity or create the new one.
My idea was to send entity's id and let it to be converted by the default entity type if user select existing entity or send something like "_new:entered text" if user enter new value. Then this string should be converted to the new form entity by my own model transformer, which should look something like this:
<?php
namespace Acme\MainBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class EmptyEntityTransformer
implements DataTransformerInterface
{
private $entityName;
public function __construct($entityName)
{
$this->entityName = $entityName;
}
public function transform($val)
{
return $val;
}
public function reverseTransform($val)
{
$ret = $val;
if (substr($val, 0, 5) == '_new:') {
$param = substr($val, 5);
$ret = new $this->entityName($param);
}
return $ret;
}
}
Unfortunately, the transformer is only called when existing entity is selected. When I enter a new value, the string is sent in the request but transformer's reverseTransform method is not called at all.
I'm new to Symfony so I don't even know if this approach is correct. Do you have any Idea how to solve this?
edit:
My form type code is:
<?php
namespace Acme\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Acme\MainBundle\Form\DataTransformer\EmptyEntityTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class Select2EntityType
extends AbstractType
{
protected $router;
public function __construct(Router $router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->setDefaults(array(
'placeholder' => null,
'path' => false,
'pathParams' => null,
'allowNew' => false,
'newClass' => false,
));
}
public function getParent()
{
return 'entity';
}
public function getName()
{
return 's2_entity';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['newClass']) {
$transformer = new EmptyEntityTransformer($options['newClass']);
$builder->addModelTransformer($transformer);
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$field = $view->vars['name'];
$parentData = $form->getParent()->getData();
$opts = array();
if (null !== $parentData) {
$accessor = PropertyAccess::createPropertyAccessor();
$val = $accessor->getValue($parentData, $field);
if (is_object($val)) {
$getter = 'get' . ucfirst($options['property']);
$opts['selectedLabel'] = $val->$getter();
}
elseif ($choices = $options['choices']) {
if (is_array($choices) && array_key_exists($val, $choices)) {
$opts['selectedLabel'] = $choices[$val];
}
}
}
$jsOpts = array('placeholder');
foreach ($jsOpts as $jsOpt) {
if (!empty($options[$jsOpt])) {
$opts[$jsOpt] = $options[$jsOpt];
}
}
$view->vars['allowNew'] = !empty($options['allowNew']);
$opts['allowClear'] = !$options['required'];
if ($options['path']) {
$ajax = array();
if (!$options['path']) {
throw new \RuntimeException('You must define path option to use ajax');
}
$ajax['url'] = $this->router->generate($options['path'], array_merge($options['pathParams'], array(
'fieldName' => $options['property'],
)));
$ajax['quietMillis'] = 250;
$opts['ajax'] = $ajax;
}
$view->vars['options'] = $opts;
}
}
and then I create this form type:
class EditType
extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('masterProject', 's2_entity', array(
'label' => 'Label',
'class' => 'MyBundle:MyEntity',
'property' => 'name',
'path' => 'my_route',
'pathParams' => array('entityName' => 'name'),
'allowNew' => true,
'newClass' => '\\...\\MyEntity',
))
...
Thanks for your suggestions
I think I found an answer however I'm not really sure if this is the correct solution. When I tried to understand how EntityType works I noticed that it uses EntityChoiceList to retrive list of available options and in this class there is a getChoicesForValues method which is called when ids are transformed to entities. So I implemented my own ChoiceList which adds my own class to the end of the returned array:
<?php
namespace Acme\MainBundle\Form\ChoiceList;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class EmptyEntityChoiceList
extends EntityChoiceList
{
private $newClassName = null;
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null, $newClassName = null)
{
parent::__construct($manager, $class, $labelPath, $entityLoader, $entities, $preferredEntities, $groupPath, $propertyAccessor);
$this->newClassName = $newClassName;
}
public function getChoicesForValues(array $values)
{
$ret = parent::getChoicesForValues($values);
foreach ($values as $value) {
if (is_string($value) && substr($value, 0, 5) == '_new:') {
$val = substr($value, 5);
if ($this->newClassName) {
$val = new $this->newClassName($val);
}
$ret[] = $val;
}
}
return $ret;
}
}
Registering this ChoiceList to the form type is a bit complicated because the class name of original choice list is hardcoded in the DoctrineType which EntityType extends but it is not difficult to understand how to do it if you have a look into this class.
The reason why DataTransformer is not called probably is that EntityType is capable to return array of results and transform is applied to every item of this collection. If the result array is empty, there is obviously no item to call transformer on.
I had exactly the same question as yours, I chose to use an FormEvent with still a DataTransformer
The idea is to switch the field type (the entity one) just before the submit.
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (substr($data['project'], 0, 5) == '_new:') {
$form->add('project', ProjectCreateByNameType::class, $options);
}
}
This will replace the project field with a new custom one before the submit if needed.
ProjectCreateByNameType can extend a TextField and have to add the DataTransformer.

Zend form and annotations validation

I'm trying using annotatnions to build and validate zend forms.
But currently I recive an error when I open showformAction:
"Fatal error: Uncaught exception 'Zend\Form\Exception\InvalidElementException' with message 'No element by the name of [username] found in form' ..."
So below is my code. What I doing wrong ?
Entity\User.php
namespace Application\Model;
use Zend\Form\Annotation;
/**
* #Annotation\Hydrator("Zend\Stdlib\Hydrator\ObjectProperty")
* #Annotation\Name("user")
*/
class User
{
/**
* #Annotation\Attributes({"type":"text" })
* #Annotation\Validator({"type":"Regex","options":{"regex":"/^[a-zA-Z][a-zA-Z0-9_-]{1,19}/"}})
* #Annotation\Options({"label":"Username:"})
*/
public $username;
}
Controller\ProductsController.php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Json\Json;
use Zend\View\Model\JsonModel;
use Zend\View\Model\ViewModel;
use Zend\Debug\Debug;
use Application\Entity\Products;
use Application\Entity\Category;
use Application\Form\ProductsForm;
use Doctrine\ORM\EntityManager;
use Application\Model\User;
use Zend\Form\Annotation\AnnotationBuilder;
class ProductsController extends AbstractActionController {
protected $albumTable;
protected $em;
protected $form;
public function savetodb($data) {
//code save to db ....
}
protected function getForm() {
$entity = new User();
$builder = new AnnotationBuilder();
$this->form = $builder->createForm($entity);
return $this->form;
}
public function showformAction() {
$viewmodel = new ViewModel();
$form = $this->getForm();
$request = $this->getRequest();
//disable layout if request by Ajax
$viewmodel->setTerminal($request->isXmlHttpRequest());
$is_xmlhttprequest = 1;
if (!$request->isXmlHttpRequest()) {
//if NOT using Ajax
$is_xmlhttprequest = 0;
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
//save to db <span class="wp-smiley emoji emoji-wink" title=";)">;)</span>
$this->savetodb($form->getData());
}
}
}
$viewmodel->setVariables(array(
'form' => $form,
// is_xmlhttprequest is needed for check this form is in modal dialog or not
// in view
'is_xmlhttprequest' => $is_xmlhttprequest
));
return $viewmodel;
}
public function validatepostajaxAction() {
$form = $this->getForm();
$request = $this->getRequest();
$response = $this->getResponse();
$messages = array();
if ($request->isPost()) {
$form->setData($request->getPost());
if (!$form->isValid()) {
$errors = $form->getMessages();
foreach ($errors as $key => $row) {
if (!empty($row) && $key != 'submit') {
foreach ($row as $keyer => $rower) {
//save error(s) per-element that
//needed by Javascript
$messages[$key][] = $rower;
}
}
}
}
if (!empty($messages)) {
$response->setContent(\Zend\Json\Json::encode($messages));
} else {
//save to db <span class="wp-smiley emoji emoji-wink" title=";)">;)</span>
$this->savetodb($form->getData());
$response->setContent(\Zend\Json\Json::encode(array('success' => 1)));
}
}
return $response;
}
}
Your annotation should be
/**
* #Annotation\Type("Zend\Form\Element\Text")
* #Annotation\Validator({"type":"Regex","options":{"regex":"/^[a-zA-Z][a-zA-Z0-9_-]{1,19}/"}})
* #Annotation\Options({"label":"Username:"})
*/

Laravel model event saving is not firing

I'm trying to simulate what Ardent package is doing. Which is validating a model right before saving.
I've created this BaseModel (According to Laravel Testing decoded book). And added this code :
class BaseModel extends Eloquent {
protected static $rules = [];
public $errors = [];
public function validate(){
$v = Validator::make($this->attributes, static::$rules);
if($v->passes()) {
return true;
}
$this->errors = $v->messages();
return false;
}
public static function boot(){
parent::boot();
static::saving(function($model){
if($model->validate() === true){
foreach ($model->attributes as $key => $value) {
if(preg_match("/[a-zA-Z]+_confirmation/", $key)){
array_splice($model->attributes, array_search($key, array_keys($model->attributes)), 1);
}
}
echo "test"; //This is for debugging if this event is fired or not
return true;
} else {
return false;
}
});
}
}
Now, this is my Post model :
class Post extends BaseModel {
public static $rules = array(
'body' => 'required',
'user_id' => 'required',
);
}
In this test i'm expecting it to fail. Instead, it passes ! , $post->save() returns true !
class PostTest extends TestCase {
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
When i tried to throw an echo statement inside the saving event. It didn't appear, So i understand that my defined saving event is not invoked. I don't know why.
check out this discussion: https://github.com/laravel/framework/issues/1181
you'll probably need to re-register your events in your tests.
class PostTest extends TestCase {
public function setUp()
{
parent::setUp();
// add this to remove all event listeners
Post::flushEventListeners();
// reboot the static to reattach listeners
Post::boot();
}
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
Or, better yet, you should extract the event registration functionality out of the boot function into a public static method:
class Post extends Model {
protected static boot()
{
parent::boot();
static::registerEventListeners();
}
protected static registerEventListeners()
{
static::saving(...);
static::creating(...);
...etc.
}
}
And then call Post::flushEventListeners(); Post::registerEventListeners(); in the setUp() test method.
The saving event looks fine for me. The validation fails, so $post->save() returns false. Your test passes because you expect $post->save() to be false (assertFalse), which in this case is correct.
Try these tests instead.
public function testSavingInvalidPost() {
$post = new Post();
$this->assertFalse($post->save());
}
public function testSavingValidPost() {
$post = new Post();
$post->body = 'Content';
$post->user_id = 1;
$this->assertTrue($post->save());
}

How to make 2 forms in one view dealing with the same entity but with only one bind to it?

Here is the thing.
I want to make 2 forms in one view. One is bound to an entity and the other is a file type where I want to put .csv and catch the informations inside to fill the first entity.
I've already created the first one but I can't figure how to create the second and integrate it correctly in the same view so they don't fight each other. Here is my files (the csv process is not here yet)
My controller:
public function adminAction()
{
$form = $this->createForm(new StudentsType, null);
$formHandler = new StudentsHandler($form, $this->get('request'), $this->getDoctrine()->getEntityManager());
if( $formHandler->process() )
{
return $this->redirect( $this->generateUrl('EnsgtiEnsgtiBundle_ens'));
}
return $this->render('EnsgtiEnsgtiBundle:Appli:admin.html.twig', array(
'form' => $form->createView(),
));
}
First form:
class StudentsType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('nom', 'text')->setRequired(false)
->add('prenom', 'text')
->add('email', 'email')
->add('codeEtape', 'text')
->add('file', new StudentsListType)->SetRequired(false);
}
public function getName()
{
return 'ensgti_ensgtibundle_studentstype';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Ensgti\EnsgtiBundle\Entity\Students',
);
}
}
The Handler:
class StudentsHandler
{
protected $form;
protected $request;
protected $em;
public function __construct(Form $form, Request $request, EntityManager $em)
{
$this->form = $form;
$this->request = $request;
$this->em = $em;
}
public function process()
{
if( $this->request->getMethod() == 'POST' )
{
$this->form->bindRequest($this->request);
if( $this->form->isValid() )
{
$this->onSuccess($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(Students $students)
{
$this->em->persist($students);
$this->em->flush();
}
}
Second Form:
class StudentsListType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('', 'file');
}
public function getName()
{
return 'ensgti_ensgtibundle_studentslisttype';
}
}
The Handler:
class StudentsListHandler
{
protected $form;
protected $request;
protected $em;
public function __construct(Form $form, Request $request, EntityManager $em)
{
$this->form = $form;
$this->request = $request;
$this->em = $em;
}
public function process()
{
if( $this->request->getMethod() == 'POST' )
{
$this->form->bindRequest($this->request);
if( $this->form->isValid() )
{
//$this->onSuccess($this->form->getData());
print_r($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(/*StudentsList $studentsList*/)
{
//$this->em->persist($studentsList);
//$this->em->flush();
echo 'Petit test';
}
}
With this code I get this:
Neither property "file" nor method "getFile()" nor method "isFile()" exists in class "Ensgti\EnsgtiBundle\Entity\Students"
Which is normal since I'm bind to an entity with no file type. I'm a beginner so I keep reading docs but it's still hard for me to understand everything. Is there a way I can make 2 forms in the same view with one that is not bind to an entity but that will persist on it anyway via csv treatment??
The problem is that the form expects file to be a property of the bound entity. You just need to set the property_path to false. (Reference) e.g.
->add('file', new StudentsListType, array('property_path' => false))
Edit:
If it would make more sense to have the 2 forms completely separate, then remove the above ->add(/*...*/) entirely and just pass both forms to the view from the controller. You can then render each one individually.
use <iframe> to do that. Here is the link how to use it, just put in src atribute url to needed page. And you may display 2 pages in one layout. And this is how it looks

Categories