I need to set up a custom form type in Symfony that uses the choice type as a parent but doesn't actually require choices to be preloaded. As in I want to be able to populate the select with an ajax call and then submit with one of the options from the call without getting This value is not valid. errors, presumably because its not one of the preloaded options.
I don't need a custom data transformer as I am doing that through the bundle controller, I just need Symfony not to complain when I submit with an option that wasn't originally on the list. Here is what my custom form type looks like so far:
<?php
namespace ISFP\Index\IndexBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class NullEntityType extends AbstractType
{
public function getDefaultOptions(array $options)
{
$defaultOptions = array(
'em' => null,
'class' => null,
'property' => null,
);
$options = array_replace($defaultOptions, $options);
return $options;
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'null_entity';
}
}
Dude look at the EntityType it has a parent as a choice. But entire display was handle by ChoiceType. When I was doing similar things I've started from overload Both ChoiceType and EntityType. And then set in overloaded Entity the getParent() to mine overloaded choice.
Finally In my case I modify the new choice and put there my embedded form. It's tricky to do It. And it consumes lot's of time.
But with that approach i don't have any problem with Validation.
Related
I'm working on a TagField for EasyAdmin 4 (and Symfony 6) that will rely on a TagType. This TagType will have the native ChoiceType as a parent.
This field will be rendered as a multiple select, with these attributes to allow adding tags on the fly:
[ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ]
To do so, I created a TagListener. Its main goal is to prefill the options with the already existing tags (on other entities) to support tag suggestion. After reading the docs and many articles, I chose to listen to the FormEvents::PRE_SET_DATA event.
Unfortunately there does not seem to be an easy way to "override" the default options, and we're left with having to override the entire field.
Here's what the TagListener looks like:
<?php
// src/Form/EventListener/TagListener.php
namespace eduMedia\TagBundle\Form\EventListener;
use eduMedia\TagBundle\Entity\TaggableInterface;
use eduMedia\TagBundle\Form\Type\TagType;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TagListener implements EventSubscriberInterface
{
public function __construct(private TagService $tagService)
{
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
];
}
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$parentForm = $event->getForm()->getParent();
/** #var TaggableInterface $taggable */
$taggable = $parentForm->getData();
// We retrieve the existing options to override some of them
$options = $form->getConfig()->getOptions();
// if ($options['pre_set_data_called']) {
// return;
// }
// We prefill options with the existing tags for this resource type
$allTagNames = $this->tagService->getTypeTagNames($taggable->getTaggableType());
// They are our new choices
$options['choices'] = array_combine($allTagNames, $allTagNames);
// We also need to select the entity's tags
$options['data'] = $this->tagService->loadTagging($taggable)->getTagNames($taggable);
// We override the form field
// $options['pre_set_data_called'] = true;
$parentForm->add($form->getName(), TagType::class, $options);
}
}
Doing so seems to create an infinite loop, where onPreSetData is called when calling $parentForm->add(). Is that normal? Is PRE_SET_DATA dispatched again when adding a field in a listener? Is there a way to prevent this from happening?
I tried adding a pre_set_data_called form option, setting it to true when calling $parentForm->add() and exiting the listener when it is indeed true. It kind of works, but then I get this error:
An exception has been thrown during the rendering of a template ("Field "tags" has already been rendered, save the result of previous render call to a variable and output that instead.").
How can I manage to allow extra items in my custom field type?
For reference, here is my TagType class:
<?php
namespace eduMedia\TagBundle\Form\Type;
use eduMedia\TagBundle\Form\EventListener\TagListener;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function __construct(private TagService $tagService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new TagListener($this->tagService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false,
'multiple' => true,
// 'pre_set_data_called' => false,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}
And my TagField class:
<?php
namespace eduMedia\TagBundle\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use eduMedia\TagBundle\Form\Type\TagType;
class TagField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null)
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TagType::class)
->setFormTypeOption('attr', [ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ])
->setTemplatePath('#eduMediaTag/fields/tag.html.twig')
;
}
}
I ended up not using the ChoiceType as the parent (<select> element), but rather the TextType (<input type=text> element), and splitting/exploding a simple string.
The actual bundle is live on GitHub and even though it might not be perfect (yet 😉), the implementation is way simpler and the end-user behaviour is exactly what I expected.
So i tried to store the id of one of my entities in the hiddenType and i got:
The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class AppBundle\Entity\Users. You can avoid this error by setting the "data_class" option to "AppBundle\Entity\Users" or by adding a view transformer that transforms an instance of class AppBundle\Entity\Users to scalar, array or an instance of \ArrayAccess.
data_class: "This option is used to set the appropriate data mapper to be used by the form, so you can use it for any form field type which requires an object."
see: http://symfony.com/doc/2.7/reference/forms/types/form.html#data-class
and so i fix my form:
$builder
->add('user', 'hidden', array(
'data_class' => 'AppBundle\Entity\User',
));
when i try this i get an exception stating that my entity cannot be translated to a string
so i implement the __tostring magic method on my entity to return the entity’s id, then twig is able to put the entity id in the hidden field value
then when i try to submit my form i get:
Catchable Fatal Error: Argument 1 passed to AppBundle\Entity\Students::setUser() must be an instance of AppBundle\Entity\Users, string given, called in /vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php on line 442 and defined
so its not able to pull the string value from the request back into an entity for use in my form.
yes, i've seen the implementation where you build a entityHiddenType using a transformer.
however i'm asking is this possible using the data_class setting provided by symphony as i believe this is the intended method to solve this?
I just want to know if it can be achieved using data_class instead of a transformer. as well as which method is best practice.
I had the same issue, I solved it by setting the data_class to null in my for my HiddenType:
<?php namespace AppBundle\Forms\Signup;
use AppBundle\Entity\Course;
use AppBundle\Repository\CourseRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PreselectedCourseType extends AbstractType
{
private $courseRepository;
public function __construct(CourseRepository $courseRepository)
{
$this->courseRepository = $courseRepository;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$selectedCourse = $options['selected_course'];
$builder
->add("course", HiddenType::class,['data' => $selectedCourse, 'data_class' => null]);
$builder->get("course")->addModelTransformer(new CallbackTransformer(
function (Course $course = null) {return $course? $course->getId():0;},
function ($course = null) {return $this->courseRepository->getCourse($course);}
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CourseDTO::class,
'label' => false,
'selected_course' => 0
]);
$resolver->setRequired("selected_course");
}
}
I'm using Symfony forms (v3.0) without the rest of the Symfony framework. Using Doctrine v2.5.
I've created a form, here's the form type class:
class CreateMyEntityForm extends BaseFormType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('myEntity', EntityType::class);
}
}
When loading the page, I get the following error.
Argument 1 passed to
Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must be
an instance of Doctrine\Common\Persistence\ManagerRegistry, none
given, called in /var/www/dev3/Vendor/symfony/form/FormRegistry.php on
line 85
I believe there's some configuration that needs putting in place here, but I don't know how to create a class that implements ManagerRegistryInterface - if that is the right thing to do.
Any pointers?
Edit - here is my code for setting up Doctrine
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
class Bootstrap {
//...some other methods, including getCredentials() which returns DB credentials for Doctrine
public function getEntityManager($env){
$isDevMode = $env == 'dev';
$paths = [ROOT_DIR . '/src'];
$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode, null, null, false);
$dbParams = $this->getCredentials($env);
$em = EntityManager::create($dbParams, $config);
return $em;
}
}
Believe me, you're asking for trouble!
EntityType::class works when it is seamsly integrated to "Symfony" framework (there's magic under the hoods - via DoctrineBundle). Otherwise, you need to write a lot of code for it to work properly.
Not worth the effort!
It's a lot easier if you to create an entity repository and inject it in form constructor, then use in a ChoiceType::class field. Somethink like this:
<?php
# you form class
namespace Application\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class InvoiceItemtType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('product', ChoiceType::class, [
'choices' => $this->loadProducts($options['products'])
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['products' => [],]); # custom form option
}
private function loadProducts($productsCollection)
{
# custom logic here (if any)
}
}
And somewhere in application:
$repo = $entityManager->getRepository(Product::class);
$formOptions = ['products' => $repo->findAll()];
$formFactory = Forms::createFormFactory();
$formFactory->create(InvoiceItemtType::class, new InvoiceItem, $formOptions);
That's the point!
Expanding on the answer by xabbuh.
I was able to implement EntityType in the FormBuilder without too much extra work. However it does not work with the annotations in order to use Constraints directly inside the entity, which would require a lot more work.
You can easily facilitate the ManagerRegistry requirement of the Doctrine ORM Forms Extension, by extending the existing AbstractManagerRegistry and making your own container property within the custom ManagerRegistry.
Then it's just a matter of registering the Form extension just like any other extension (ValidatorExtension, HttpFoundationExtension, etc).
The ManagerRegistry
use \Doctrine\Common\Persistence\AbstractManagerRegistry;
class ManagerRegistry extends AbstractManagerRegistry
{
/**
* #var array
*/
protected $container = [];
public function __construct($name, array $connections, array $managers, $defaultConnection, $defaultManager, $proxyInterfaceName)
{
$this->container = $managers;
parent::__construct($name, $connections, array_keys($managers), $defaultConnection, $defaultManager, $proxyInterfaceName);
}
protected function getService($name)
{
return $this->container[$name];
//alternatively supply the entity manager here instead
}
protected function resetService($name)
{
//unset($this->container[$name]);
return; //don't want to lose the manager
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported');
}
}
Create the Form
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('field_name', EntityType::class, [
'class' => YourEntity::class,
'choice_label' => 'id'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => YourAssociatedEntity::class]);
}
}
Configure the Form Builder to use the extension and use the Form
$managerRegistry = new \ManagerRegistry('default', [], ['default' => $entityManager], null, 'default', 'Doctrine\\ORM\\Proxy\\Proxy');
$extension = new \Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($managerRegistry);
$formBuilder = \Symfony\Component\Form\FormFactoryBuilder::createFormFactoryBuilder();
$formBuilder->addExtension($extension);
$formFactory = $formBuilder->getFormFactory();
$form = $formFactory->create(new \UserType, $data, $options);
The above is intended for demonstration purposes only! While it does
function, it is considered best
practice to
avoid using Doctrine Entities inside of Forms. Use a DTO (Data
Transfer Object) instead.
ENTITIES SHOULD ALWAYS BE VALID
INVALID STATE SHOULD BE IN A DIFFERENT OBJECT
(You may need a DTO)
(Also applies to Temporary State)
AVOID SETTERS
AVOID COUPLING WITH THE APPLICATION LAYER
FORM COMPONENTS BREAK ENTITY VALIDITY
BOTH SYMFONY\FORM AND ZEND\FORM ARE TERRIBLE
(For this use-case)
Use a DTO instead
Doctrine 2.5+ "NEW" Operator Syntax
class CustomerDTO
{
public function __construct($name, $email, $city, $value = null)
{
// Bind values to the object properties.
}
}
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city) FROM Customer c JOIN c.email e JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
The easiest way to solve your issue is by registering the DoctrineOrmExtension from the Doctrine bridge which makes sure that the entity type is registered with the needed dependencies.
So basically, the process of bootstrapping the Form component would look like this:
// a Doctrine ManagerRegistry instance (you will probably already build this somewhere else)
$managerRegistry = ...;
$doctrineOrmExtension = new DoctrineOrmExtension($managerRegistry);
// the list of form extensions
$extensions = array();
// register other extensions
// ...
// add the DoctrineOrmExtension
$extensions[] = $doctrineOrmExtension;
// a ResolvedFormTypeFactoryInterface instance
$resolvedTypeFactory = ...;
$formRegistry = new FormRegistry($extensions, $resolvedTypeFactory);
I need to programmatically change the behaviour of a form based on some options. Let's say, for example, I'm displaying a form with some user's info.
I need to display a checkbox, "send mail", if and only if a user has not received an activation mail yet. Previously, with ZF1, i used to do something like
$form = new MyForm(array("displaySendMail" => true))
which, in turn, was received as an option, and which allow'd to do
class MyForm extends Zend_Form {
protected $displaySendMail;
[...]
public function setDisplaySendMail($displaySendMail)
{
$this->displaySendMail = $displaySendMail;
}
public function init() {
[....]
if($this->displaySendMail)
{
$displaySendMail new Zend_Form_Element_Checkbox("sendmail");
$displaySendMail
->setRequired(true)
->setLabel("Send Activation Mail");
}
}
How could this be accomplished using Zend Framework 2? All the stuff I found is about managing dependencies (classes), and nothing about this scenario, except this SO question: ZF2 How to pass a variable to a form
which, in the end, falls back on passing a dependency. Maybe what's on the last comment, by Jean Paul Rumeau could provide a solution, but I wasn't able to get it work.
Thx
A.
#AlexP, thanks for your support. I already use the FormElementManager, so it should be straightforward. If I understand correctly, I should just retrieve these option in my SomeForm constructor, shouldn't I?
[in Module.php]
'Application\SomeForm' => function($sm)
{
$form = new SomeForm();
$form->setServiceManager($sm);
return $form;
},
while in SomeForm.php
class SomeForm extends Form implements ServiceManagerAwareInterface
{
protected $sm;
public function __construct($name, $options) {
[here i have options?]
parent::__construct($name, $options);
}
}
I tryed this, but was not working, I'll give it a second try and double check everything.
With the plugin managers (classes extending Zend\ServiceManager\AbstractPluginManager) you are able to provide 'creation options' array as the second parameter.
$formElementManager = $serviceManager->get('FormElementManager');
$form = $formElementManager->get('SomeForm', array('foo' => 'bar'));
What is important is how you have registered the service with the manager. 'invokable' services will have the options array passed into the requested service's constructor, however 'factories' (which have to be a string of the factory class name) will get the options in it's constructor.
Edit
You have registered your service with an anonymous function which mean this will not work for you. Instead use a factory class.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'Application\SomeForm' => 'Application\SomeFormFactory',
),
);
}
An then it's the factory that will get the options injected into it's constructor (which if you think about it makes sense).
namespace Application;
use Application\SomeForm;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class SomeFormFactory implements FactoryInterface
{
protected $options = array();
public function __construct(array $options = array())
{
$this->options = $options;
}
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new SomeForm('some_form', $this->options);
}
}
Alternatively, you can inject directly into the service you are requesting (SomeForm) by registering it as an 'invokeable' service; obviously this will depend on what dependencies the service requires.
I'm trying to build a Symfony form (in Silex) by name. Using the configuration below, I believe I should be able to call $form = $app['form.factory']->createBuilder('address');, however the FormRegistry cannot find a form of this type.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
class AddressType extends AbstractType implements FormTypeExtensionInterface
{
public function getName()
{
return 'address';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('addressee', 'text');
// .. fields ..
$builder->add('country', 'text');
}
public function getExtendedType()
{
return 'form';
}
}
This is then added to the form registry using the form.type.extensions provider:
$app['form.type.extensions'] = $app->share($app->extend('form.type.extensions', function($extensions) use ($app) {
$extensions[] = new AddressType();
return $extensions;
}));
Is there something else I need to do or a different way of building the form in this way?
Why not use direct
$app['form.factory']->createBuilder('Namespace\\Form\\Types\\Form')
First, sorry for my poor english. :)
I think you should extend form.extensions, instead of form.type.extensions.
Something like this:
$app['form.extensions'] = $app->share($app->extend('form.extensions',
function($extensions) use ($app) {
$extensions[] = new MyTypesExtension();
return $extensions;
}));
Then your class MyTypesExtension should look like this:
use Symfony\Component\Form\AbstractExtension;
class MyTypesExtension extends AbstractExtension
{
protected function loadTypes()
{
return array(
new AddressType(),
//Others custom types...
);
}
}
Now, you can retrieve your custom type this way:
$app['form.factory']->createBuilder('address')->getForm();
Enjoy it!
I see, this question is quite old but:
What you do is creating a new Form Type not extending an existing one, so the correct way to register it to add it to the 'form.types'. (Remember: form type extension is adding something to the existing types so for the future all instance will have that new 'feature'. Here you are creating a custom form type.)
$app['form.types'] = $app->share($app->extend('form.types', function ($types) use ($app) {
$types[] = new AddressType();
return $types;
}));
I think when you are coming from Symfony to Silex form.type.extension can be misleading.
From Symfony How to Create a Form Type Extension:
You want to add a generic feature to several types (such as adding a "help" text to every field type);
You want to add a specific feature to a single type (such as adding a "download" feature to the "file" field type).
So as your code shows you want to add a FormType which exists in Symfony but you would use the FormServiceProvider in Silex without defining an AbstractType and just use the form.factory service as shown in this example:
In your app.php:
use Silex\Provider\FormServiceProvider;
$app->register(new FormServiceProvider());
In your controller/action:
$form = $app['form.factory']->createBuilder('form', $data)
->add('name')
->add('email')
->add('gender', 'choice', array(
'choices' => array(1 => 'male', 2 => 'female'),
'expanded' => true,
))
->getForm()
;