I am trying to build a form for multiple entities. Let me first introduce some sample classes:
For clarity I do not show all annotations or abbreviate them, and I do not show the use or namespace commands.
/**
* The Entity class
* #ORM ... mapping to ORM
*/
class EntityA {
/**
* #var ModelArray
* #ORM\Column(name="...", type="object")
*/
private $modelArray;
// Getters and Setters, default constructor
}
/**
* An array Wrapper, keeping the array always unique, sorting it by criteria etc.
* #ORM(...)
*/
class ModelArray {
/**
* #var array<A>
*/
private $array;
// Getter, Adder, Remover, Constructor, other private logic
}
Notice especially that the class ModelArray stores only objects of a given Type A:
/**
* One more Model
*/
class A {
/**
* #var boolean
*/
private $bool;
/**
* #var string
*/
private $string;
// Getters, Setters
}
I chose this data structure, because I never need objects of A to exist outside the EntityA class, and to keep the logic out of my Entity, I chose to implement the ModelArray class in between. The EntityA class is persisted to a database, and with it the child objects.
Now I want to create a form, where I can edit the $->bool attributes of all A instances of an Array of EntityA at once.
So what I provide is array<EntityA>.
I would then proceed as follows:
//In a controller action
$data = array(
'as' => $arrayOfEntityA,
);
$form = $this->createForm(new GridType(), $data);
// Create view, render
My form type would look like this:
class GridType extends AbstractType {
public function getName() {
return 'a_grid';
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('as', 'collection', array(
'type' => new GridAType(),
));
}
}
And use this form type
class GridAType extends AbstractType {
public function getName() {
return 'a_grid_a';
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('modelArray', new ModelArrayType());
}
public function getDefaultOptions(array $options) {
return array(
'data_class' => 'EntityA',
);
}
}
With the model array type
class ModelArrayType extends AbstractType {
public function getName() {
return 'model_array';
}
public function buildForm(FormBuilderInterface $builder, array $options) {
// ???
}
public function getDefaultOptions(array $options) {
return array(
'data_class' => 'ModelArray',
);
}
}
Notice the // ???: I would like to only edit the boolean attribute of the As strored in the ModelArray, and I think the appropriate form type to continue would be a 'collection'. But I can't really find out how to use the array ($modelArray->array)here as a collection.
How should I do this?
An then, I'm not entirely sure if it is good practice to implement such a lot of form types just to achieve one usable form. Is there a different and better way?
Related
I have 2 entities A and B that share common fields, I used a trait to setup those common fields based on (Doctrine inheritance for entities common fields) because I don't want to use a MappedSuperClass.
Setting up a restful post route for entity B, I instantiate a FormBType which data_class maps to B::class, that extends FormCType (contains common fields and 'data_class' maps to nothing).
I tried to use the inherit_data approach with https://symfony.com/doc/current/form/inherit_data_option.html but I don't want that extra key/nested layer in my form (I want a flattened one).
My problem is that validation for the common fields which are in the trait using Assert aren't taken into account and form passes validation with empty strings.
class B {
use CTrait;
}
//trait that has the common fields with ORM mapping and Assert
trait CTrait {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string")
* #ORM\Assert\Length(min="2")
*/
private $name;
}
//Common fields formType
class CType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
}
//Form using the common fields formType
class BType extends CType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => B::class,
'csrf_protection' => false,
]);
}
}
After further checking on Length I realized empty strings are considered valid values and it was still passing through validation using name: "" even though I had Assert\Length(min=2), after adding NotBlank the validation worked.
I have created a FormType that get's a data_class set in the defaults in the configureOptions class. It looks a bit like this
<?php
namespace tzfrs\AppBundle\Form\Type;
use tzfrs\AppBundle\Model\Car;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CarType
* #package tzfrs\AppBundle\Form\Type
*/
class CarType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('specification', ChoiceType::class, [
'choices' => [1 => 1, 2 => 2, 3 => 3]
]);
}
/**
* #inheritdoc
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Car::class
]);
}
}
When I now create the form like this
$carForm = $this->createForm(CarType::class, $car);
and want to use it I get the following error:
The value of type "object" cannot be converted to a valid array key.
I think the reason for this, is because my Car class has a property specification with getters/setters and therefore returns a Specification object. However, my Specification object has a __toString() method.
Car.php
/**
* #var Specification
*/
protected $specification;
public function getSpecification(): Specification
{
return $this->specification;
}
public function setSpecification(Specification $specification): Car
{
$this->specification = $specfification;
return $this;
}
Specification.php
/**
* Returns a string implementation of the current class
*
* #return string
*/
public function __toString(): string
{
return sprintf(
'HP: %s,CCM: %s',
$this->getHorsePower(),
$this->getCapacity()
);
}
How would I achieve overriding my choices for a form field which uses a custom Model? I can't use the EntityType because it's not a Doctrine Model.
When I build my form like this
$carForm = $this->createForm(Car::class);
then it works, but I need the Car object inside the Form, because of another field I'm adding in the buildForm method
I have this configuration :
ModuleModelType
| - title
| - created
| - subform
| - DevField1
| - DevField2
| - etc...
What I want to have is something like this :
ModuleModelType
| - title
| - created
| - subform
| | - DevField1
| | - DevField2
| | - etc...
But the problem is that i don't know what users will add to the form, as I'm creating a reusable bundle and I don't want name conflicts.
This is my code, how I can make this easily ?
I want a developer can create a class and append his own form in an existing form.The following existing code append the dev's form into the first one and can replace some existing keys if keys are the same.
Actually, I want to append all dev's fields into and subform to avoid this.
<?php
// Into a controller
/** #var ModuleInterface $instance */
$instance = ... ;
$form = $this->createForm(ModuleModelType::class);
$subform= $this->createForm(new FormType());
$instance->buildForm($subform);
$form->add('subform',$subform); // I want to make something like that
interface ModuleInterface
{
public function buildForm(Form &$form);
}
class Foo implements ModuleInterface
{
public function buildForm(Form &$builder)
{
$builder->add("DevField1", /* type */);
$builder->add("DevField2", /* type */);
}
}
class ModuleModelType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('created')
->add('subform')
;
}
}
We will assume some facts :
You want to have an entity with "data" field
This "data" field must be an array
A form must be changed to handle this "data" field
Obviously, in the entity, we can load and save "data" informations, as an array
This behavior can be easily done passing the subform by constructor.
Your problem happen because you're fighting against Symfony, please considere a standard way :
Have an entity with "array" field type
Have two forms Type : First one to handle your entity, second one to handle the embedded form.
Use dependency injection to embed the right form. (Assume two services by Module : first one for the embedded form, second one for you.
Your entity : (Think to generate getters & setters with generate:doctrine:entities)
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Foo
*
* #ORM\Table(name="foo")
* #ORM\Entity(repositoryClass="AppBundle\Repository\FooRepository")
*/
class Foo
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="some_informations", type="string", length=255)
*/
private $someInformations;
/**
* #var string
*
* #ORM\Column(name="Data", type="string", length=255)
*/
private $data;
/**
* #var string
*
* #ORM\Column(name="data", type="array")
*/
private $data;
Your form :
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FooType extends AbstractType
{
protected $dataFormName;
public function __construct($dataFormName)
{
$this->dataFormName = $dataFormName;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('someInformations')
->add('data', $this->dataFormName)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Foo'
));
}
}
Your subform :
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SubformType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('content')
->add('some_data')
;
}
}
At this point, you should be able to generate your form with :
$foo = new Foo();
$form = $this->createForm('service_name_for_the_form_with_second_embedded', $foo);
Old answer :
In this case, you should assume that ModuleInterface extends FormTypeInterface.
To be used as FormType in a $form->add, your class should look like a form.
At this point, use ModuleInterface seems to be useless.
I can purpose that way for you :
<?php
// Into a controller
$instance = new Foo();
$form = $this->createForm(new ModuleModelType($instance));
class Foo extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add("DevField1");
$builder->add("DevField2");
}
}
class ModuleModelType extends AbstractType
{
protected $subform;
function __construct(FormTypeInterface $subform)
{
$this->subform = $subform;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('created')
->add('subform', $this->subform)
;
}
}
You can also use dependency injection to inject $instance through __construct.
Take care about naming convention for "Foo" class, should be FooType ;)
If you do something like that, your expected form will be OK.
Have a nice day,
Gaƫl
I hope you already read this careful: http://symfony.com/doc/current/book/forms.html
In your case, you can do by:
use path/of/your/formType/to/embed;
class ModuleModelType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('created')
->add('subform', Foo::class)
;
}
}
Don't worry about interface. Interface just must to implement, I hope you already know that.
I dont know why you want add in controller, because you have already form class, you just need modify to make clean your code.
I you want subform as add by user, you can do by collection form embed: http://symfony.com/doc/current/cookbook/form/form_collections.html
Or you want by data (show form if submitted) : http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
NOTE: Symfony ver 3.0 are different with 2.~.
In refference to my question I asked a few hours ago, I will try to ask more specific question now.
I have followed guidelines from official cookbook, and this article.
I have added new class MyType extends AbstractType in Me\MyBundle\Controller\Form\Type direcory.
I have created a template for that custom field type.
I have added proper entry in config.yml in order to use new template in forms.
But how can I use that custom field?
Let's say I have controller which looks like this:
namespace Me\MyBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Me\MyBundle\Form\Type\MyType;
class DefaultController extends Controller
{
public function indexAction()
{
$form = $this->createFormBuilder()
->add('my_type', new MyType(), array('param' => 0))
->getForm()
;
return $this->render('MyBundle::index.html.twig', array(
'form' => $form->createView(),
));
}
}
Line ->add('my_type', new MyType(), array('param' => 0)) does not seem to have any affect on generating form. There is also no errors.
How can I make my custom field to be reused in the same form multiple times, with different params?
Edit:
MyType class looks like this:
class MyType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'param' => 1,
)
));
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'my_type';
}
}
May try to implements buildForm method :
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options
{
$builder->add(...);
}
I have a question about service and Form in Symfony2,so I created my calss form and I hope to add a multiselect list of cities then I want to get list of cities from another class "city",so how I can call my class "city" in my form using "Service" to get a function "getcities" to return me a list of cities? (I dont use Doctrine here)...
Edit
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CityType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choices' => array( /**
* Here I will call function getcities(return list of cities)
*/
)
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'gender';
}
}
class City.php :
Class City {
/**
* here i will get list of cities
*/
public function getcities()
{
.....
return $Listcities;
}
}
So I would like to use "Service" to call function "getcities" in form?
The form objects are not container-aware...at least, they're not meant to be. That said, your controller should use the service to get the cities, and then it should pass that list into the form object either through a constructor or a method.
Controller:
class SomethingController
{
public function someAction()
{
...
$cities = $this->get("citiesService")->getCities();
$form = $this->createForm(new SomeType($cities), $someEntity);
...
}
}
Form:
class SomeType extends AbstractType
{
private $cities;
public function __construct($cities)
{
$this->cities = $cities;
}
public function buildForm(FormBuilder $builder, array $options)
{
// Now you have access to $this->cities, so you can use it to build the form
}
}
Do you can set City object as form data object?
so it can looks like that...
$form = $this->createForm(new SomeType(), new City());
class SomeType extends AbstractType
{
public buildForm(FormBuilderInterface $builder, array $options)
{
$formFactory = $builder->getFormFactory();
$builder->addEventListener(
FormsEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formFactory) {
$event->getForm()->add(
$formFactory->createNamed(
'gender',
'choice',
null,
array(
'choices' => $event->getData()->getCites()
)
)
);
}
);
}
}