I have a 1 to n relationship so one Brand has many Cars. What I want to do is to create only one web form where all the fields from both of the entities get displayed. To do that I created a form type but I think I'm doing something wrong because I' getting error below when trying to print the form fields in twig. Could anyone tell me where am I doing wrong?
Error:
Method "brand" for object "Symfony\Component\Form\FormView" does not exist in CarBrandBundle:Default:both.html.twig at line 1
Entities:
class BrandEntity
{
protected $name;
protected $origin;
//Followed by getters and setters
/**
* #ORM\ManyToOne(targetEntity="BrandEntity", inversedBy="car")
* #ORM\JoinColumn(name="brand_id", referencedColumnName="id", nullable=false)
* #var object $brand
*/
protected $brand;
}
class CarEntity
{
protected $model;
protected $price;
//Followed by getters and setters
/**
* #ORM\OneToMany(targetEntity = "CarEntity", mappedBy = "brand")
* #var object $car
*/
protected $car;
public function __construct()
{
$this->car = new ArrayCollection();
}
public function addCar(\Car\BrandBundle\Entity\CarEntity $car)
{
$this->car[] = $car;
return $this;
}
public function removeCar(\Car\BrandBundle\Entity\CarEntity $car)
{
$this->car->removeElement($car);
}
}
Form Type:
namespace Car\BrandBundle\Form\Type;
use Car\BrandBundle\Entity\BrandEntity;
use Car\BrandBundle\Entity\CarEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Test\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class BothType extends AbstractType
{
public function builder(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($options['action'])
->setMethod('POST')
->add('brand', new BrandEntity())
->add('car', new CarEntity())
->add('button', 'submit', array('label' => 'Add'))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
//'data_class' => 'Car\BrandBundle\Entity\CarEntity',
'cascade_validation' => true
));
}
public function getName()
{
return 'both';
}
}
Controller:
namespace Car\BrandBundle\Controller;
use Car\BrandBundle\Form\Type\BothType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BothController extends Controller
{
public function indexAction()
{
$form = $this->getFrom();
return $this->render('CarBrandBundle:Default:both.html.twig',
array('page' => 'Both', 'form' => $form->createView()));
}
private function getFrom()
{
return $this->createForm(new BothType(), null,
array('action' => $this->generateUrl('bothCreate')));
}
}
Twig:
{{ form_row(form.brand.name) }}
{{ form_row(form.brand.origin) }}
{{ form_row(form.car.model) }}
{{ form_row(form.car.price) }}
If you want a "car form" in which you need to choose a brand, then the other answer will be ok.
If what you want is a "brand form" in which you can add /edit/delete several cars, then you need to embed a Collection of Forms.
The cookbook answer is here: http://symfony.com/doc/current/cookbook/form/form_collections.html
To render a brand form containing a collection of car forms (1-n relationship):
The form types will look like this:
The brand type
// src/Acme/TaskBundle/Form/Type/BrandType.php
//...
class BrandType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('cars', 'collection', array('type' => new CarType()));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\BrandBundle\Entity\Brand',
));
}
public function getName()
{
return 'brand';
}
}
The car type:
namespace Acme\CarBundle\Form\Car;
//...
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\CarBundle\Entity\Car',
));
}
public function getName()
{
return 'car';
}
}
Then the controller and the views just like in the cookbook. It's very powerful and easy in the end.
To add entities to a form you must use the entity field type: from the docs
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'property' => 'username',
));
In this case, all User objects will be loaded from the database and rendered as either a select tag, a set or radio buttons or a series of checkboxes (this depends on the multiple and expanded values). If the entity object does not have a __toString() method the property option is needed.
Also please post the relationships.
Related
STARTING SITUATION
I'm trying to create, with Symfony 6, a FormTypeExtension to extend my custom types.
Class hierarchy is the following:
AbstractType <-- indeed this class is abstract
+--MyBaseType
| +--MySubType1
| +--MySubType2
+--MyOtherType
My FormTypeExtension:
class TranslatableTypeExtension extends AbstractTypeExtension
{
/**
* Should apply to MyBaseType plus MySubType1 and
* MySubType2 by inheritance
*/
public static function getExtendedTypes(): iterable
{
return [
MyBaseType::class,
];
}
/**
* Special option for these custom form types
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'translate_only' => false,
]);
$resolver->setDefined(['translate_only']);
}
}
My custom type classes:
class MyBaseType extends AbstractType
{
}
class MySubType1 extends MyBaseType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('blahblahblah', TextType::class, [
'label' => 'blahblahblah',
])
(etc.)
;
}
}
(same for MySubType2)
In my controller:
class MySub1Controller extends AbstractController
{
#[Route('/MySub1/{id}/translate', name: 'app_mysub1_translate')]
public function translateMuSub1(MySub1Repository $mySub1Repository, EntityManagerInterface $entityManager, Request $request, int $id): Response
{
$mySub1 = $mySub1Repository->find($id);
$form = $this->createForm(MySubType1Type::class, $mySub1, [
'translate_only' => true,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$mySub1 = $form->getData();
$entityManager->persist($mySub1);
$entityManager->flush();
return $this->redirectToRoute('app_mysub1', ['id' => $id]);
}
return $this->renderForm('public/form/mysub1.html.twig', [
'form' => $form,
]);
}
}
This results is:
An error has occurred resolving the options of the form "App\Form\Type\MySub1Type": The option "translate_only" does not exist. Defined options are: (...)
CONCLUSION
This error message means that the FormTypeExtension does not apply to MySubType1: the option translate_only is not reckognized as allowed.
QUESTIONS
I see in the Symfony Form Type Extension doc that Form Extensions can be used for the classic Form Types inheriting FormType. But I see nowhere written that we cannot use it the same way for customer types inheriting AbstractType.
Is it possible or not?
If possible, what am I doing wrong?
Thanks in advance for your help, buddies.
FormType inheritance is built on method getParent(). Probably, you should not extend MyBaseType but return it in getParent()
getParent()
When returning a (fully-qualified) class name here, Symfony will call each method of that type (i.e. buildForm(), buildView(), etc.)
and all its type extensions, before calling the corresponding method
of your custom type.
https://symfony.com/doc/current/form/create_custom_field_type.html#creating-form-types-created-from-scratch
class MyBaseType extends AbstractType
{
}
class MySubType1 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('blahblahblah', TextType::class, [
'label' => 'blahblahblah',
])
(etc.)
;
}
public function getParent()
{
return MyBaseType::class;
}
}
In a symfony 4 form, I need to use something like a query_builder option that is available on EntityType but from a CollectionType. There is a similar question here with no good answers.
In my project, each Site entity has many Goal. Each Goal has a numeric goal and a specific date. I'd like to edit the goals of a site for a specific date only. The problem is that a CollectionType form pulls all goals to show in the form, but I only want to pull the goals for a given date. How? There is no query_builder on a CollectionType like there is on an EntityType. I could change the getter in my Site entity, but I don't know how to pass the needed date to my getter.
For now my work-around is to render the entire form (with ALL associated goals for a given site), and then use some javascript to hide all goals except those with the date to edit. This works, but it's a terrible solution for sites with lots of goals spanning a range of dates.
My Site entity (only relevant code is shown):
class Site
{
public function __construct()
{
$this->goals = new ArrayCollection();
}
/** #ORM\OneToMany(targetEntity="App\Entity\Goal", mappedBy="site") */
private $goals;
public function getGoals()
{
return $this->goals;
}
}
and my related Goal entity:
class Goal
{
/** #ORM\Column(type="date") */
private $goalDate;
/** #ORM\Column(type="integer") */
private $goal;
/** #ORM\ManyToOne(targetEntity="App\Entity\Site", inversedBy="goals") */
private $site;
// ...
}
My forms:
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class
]);
}
}
and the individual goal form:
class GoalsEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goal', IntegerType::class)
->add('goalDate', DateType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Goal::class,
]);
}
}
Using Form Events, while avoiding the allow_add and allow_delete options for the CollectionType form might land you in the right neighbourhood:
First - let's assume we're filtering by year, for ease of example, and that the year is being scooped up from a ?y=2018 style of querystring. We'll pass that info down to the form builder:
<?php
// Inside a *Action method of a controller
public function index(Request $request): Response
{
// ...
$filteredYear = $request->get('y');
$form = $this->createForm(SiteGoalsAdminForm::class, $site, ['year_filter' => $filteredYear]);
// ...
}
This implies we should be updating the default options for the SiteGoalsAdminForm class:
<?php
// SiteGoalsAdminForm.php
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
'year_filter' => 2018
]);
}
// ...
Then, in the buildForm method of that same class, we could access the Site object and remove Goals from it where the year of the goalDate did not fall inside the form's
<?php
// SiteGoalsAdminForm.php
namespace App\Form;
// ... other `use` statements, plus:
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options) {
$form = $event->getForm();
/** #var Site */
$site = $event->getData();
$goals = $site->getGoals();
foreach ($goals as $g) {
if ($g->getGoalDate()->format('Y') !== (string) $options['year_filter']) {
$site->removeGoal($g);
}
}
$form->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
);
}
// ...
}
Not a query_builder exactly, but functionally similar.
Filter the results using the entity manager in the controller that you want to set on the collection type.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);
$form = $this->createForm(SiteGoalsType::class, $site, [
'goals' => $goals
]);
Then configure the SiteGoalsType::class to accept new option goals.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
]);
$resolver->setRequired(['goals']);
}
In the buildForm method of SiteGoalsType::class Set the data to the collection type field from the options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('goals', Type\CollectionType::class, [
'entry_type' => GoalsEmbeddedType::class,
'data' => $options['goals'],
'mapped` => false
]);
}
Make sure the add the 'mapped' => false to your collection type field else it may lead to removing the records that didn't falls in the filter we have written in the controller.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);
There is an error during execute the createForm method.
InvalidArgumentException: Could not load type "ArticleType"
My symfony version is 3.3.*.
I tried to execute the createForm method with Article::class instead of ArticleType::class.
Here is my code, where is the problem?
ArticleController.php
public function createAction(Request $request)
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// ...
}
return $this->render('admin/article/create.html.twig', [
'form' => $form->createView()
]);
}
ArticleType.php
class ArticleType extends AbstractType
{
private $categoryService;
private $tagService;
public function __construct(CategoryService $categoryService, TagService $tagService)
{
$this->categoryService = $categoryService;
$this->tagService = $tagService;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/.
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
}
public function setDefaultOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'CMS\Bundle\ContentBundle\Entity\Article'
]);
}
}
Resources/config/services.yml (included in app/config/services.yml)
services:
CMS\Bundle\ContentBundle\Form\ArticleType:
arguments: ['#cms.core.service.category', '#cms.core.service.tag']
tags: [form.type]
.
It looks like your custom form class can't be found in the current namespace(s). Try adding use CMS\Bundle\ContentBundle\Form\ArticleType; (or something similar) to your controller.
I writed the simpliest Money class ever:
class Money
{
private $amount;
private $currency;
// getters and setters here
public function toArray() {
return ['amount' => $this->amount, 'currency' => $this->currency];
}
}
Then I created Doctrine custom type which extend Sonata's JsonType:
class MoneyType extends JsonType
{
/**
* #param Money $value
* #param AbstractPlatform $platform
* #return mixed|null|string
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return parent::convertToDatabaseValue($value->toArray(), $platform);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return new Money(parent::convertToPHPValue($value, $platform));
}
public function getName()
{
return 'money';
}
}
Then I added it in my config.yml:
doctrine:
dbal:
types:
json: Sonata\Doctrine\Types\JsonType
money: Enl\CurrencyBundle\Doctrine\Type\MoneyType
Then I added form type for this data piece:
class MoneyType extends AbstractType
{
// Some trivial getters and setters for dependencies here
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'enl_money';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'currencies' => $this->getPossibleCurrencies(),
'data_class' => 'Enl\CurrencyBundle\Model\Money'
]);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('currency', 'choice', ['choices' => $options['currencies'], 'required' => true])
->add('amount', 'number', ['required' => true]);
}
public function getParent()
{
return 'form';
}
}
And then I added the field to my model:
/**
* #var Money
* #ORM\Column(type="money", nullable=true)
*/
protected $price;
And finally my sonata admin form:
$form
->with('editor.dish', ['class' => 'col-md-8'])
->add('price', 'enl_money')
// blah-blah-blah
The problem is that Sonata Admin just doesn't save the form value!
I added var_dump($object); to Admin::update() method - the new value is there in object.
After that I created the simple test:
$dish = new Dish();
$dish->setPrice(new Money(7000, "BYR"));
$em = $this->getContainer()->get('doctrine.orm.entity_manager');
$em->persist($dish);
$em->flush();
It works! What should I change in my Admin class to get this working?
I have a collection field with elements of type choice in my Symfony form. Each element should have different list o choices. How can I arrange this in Symfony2? I can't use choices option because every element will have the same choices. I have seen the choice_list option which takes an object that can produce the list of options, but I don't see how it could produce a different choices for different elements in collection.
Any idea how to deal with that?
I think you need form event : http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html.
To change the default way the collection is made.
The main form is simple:
namespace Acme\Bundle\AcmeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Acme\Bundle\AcmeBundle\Form\DescriptorDumpFieldsType;
class TranscodingType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('descriptorDumpFields', 'collection', array('type' => new DescriptorDumpFieldsType()));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\Bundle\AcmeBundle\Entity\Descriptor',
);
}
public function getName()
{
return 'descriptor';
}
}
Just a simple form with a collection of sub forms.
The second one use a form subscriber who handle the form creation. (using form events)
So the first form is created normaly and add many DescriptorDumpFieldsType wich are dynamicly created.
namespace Acme\Bundle\AcmeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormTypeInterface;
use Acme\Bundle\AcmeBundle\Form\EventListener\TranscodingSubscriber;
class DescriptorDumpFieldsType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$subscriber = new TranscodingSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\Bundle\AcmeBundle\Entity\DescriptorDumpField',
);
}
public function getName()
{
return 'desc_dump_field';
}
}
The form subscriber :
namespace Acme\Bundle\AcmeBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Acme\Bundle\AcmeBundle\Entity\DumpField;
use Acme\Bundle\AcmeBundle\Form\Transcoding\DataTransformer\JsonToHumanDateTransformer;
class TranscodingSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
return array(FormEvents::SET_DATA => 'setData');
}
public function setData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (!is_null($data)) {
$this->buildForm($data, $form);
}
}
protected function buildForm($data, $form)
{
switch ($data->getDumpField()->getType()) {
case DumpField::TYPE_ENUM:
$type = 'enum'.ucfirst($data->getDumpField()->getKey());
$class = 'dump_field_'.strtolower($data->getDumpField()->getKey());
$form->add($this->factory->createNamed('collection', 'transcodings', null, array('required' => false, 'type' => $type, 'label' => $data->getDumpField()->getKey(), 'attr' => array('class' => $class))));
break;
case DumpField::TYPE_DATE:
$transformer = new JsonToHumanDateTransformer();
$class = 'dump_field_'.strtolower($data->getDumpField()->getKey());
$builder = $this->factory->createNamedBuilder('human_date', 'params', null, array('label' => $data->getDumpField()->getKey(), 'attr' => array('class' => $class)));
$builder->prependNormTransformer($transformer);
$form->add($builder->getForm());
break;
}
}
}
So you can customize the way you want, each sub-form of the collection in buildForm.