Looking for a straightforward way to add constraints dynamically to all of my form fields. So far I've hit upon the idea of using a form type extension, which kind of works: I can modify the form view and then manually check the view on form submission.
However, is there a smarter way to add real Symfony-based constraints in real-time?
(Note that the constraints need to be added to the form in real-time as the form loads based on user configuration in the database.. Predefined form groups and the like won't work.)
I would suggest to use form events.
Use the PRE_SUBMIT event to edit the form before validation.
Recreate your fields with $event->getForm()->add(...) adding your constraints.
Of course you can automatically add the listener to all form using a FormExtension which adds the listener.
EDIT : Some examples from Alsatian67/FormBundle
Your extension should looks like :
class ExtensibleExtension extends AbstractTypeExtension
{
private $extensibleSubscriber;
public function __construct($extensibleSubscriber) {
$this->extensibleSubscriber = $extensibleSubscriber;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Only apply on base form
if($builder->getForm()->isRoot())
{
$builder->addEventSubscriber($this->extensibleSubscriber);
}
}
public function getExtendedType()
{
return FormType::class;
}
}
And your EventListener / EventSubscriber should iterate on all the children :
foreach($event->getForm()->all() as $child){
$childName = $child->getName();
$type = get_class($child->getConfig()->getType()->getInnerType());
$options = $child->getConfig()->getOptions();
$options['constraints'] = array(/* ... */);
$form->add($childName,$type,$options);
}
Related
I use Form Component and have a ChoiceType field on the form which is rendered to a select field.
On the client-side I use select2 plugin which initializes a select with tags: true allowing the addition of new values to it.
But if I add a new value then a validation on the server fails with an error
This value is not valid.
because the new value isn't in the choice list.
Is there a way to allow the addition of new values to a ChoiceType field?
The problem is in a choice transformer, which erases values that don't exist in a choice list.
The workaround with disabling the transformer helped me:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('choiceField', 'choice', ['choices' => $someList]);
// more fields...
$builder->get('choiceField')->resetViewTransformers();
}
Here's an example code in case someone needs this for EntityType instead of the ChoiceType. Add this to your FormType:
use AppBundle\Entity\Category;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!$data) {
return;
}
$categoryId = $data['category'];
// Do nothing if the category with the given ID exists
if ($this->em->getRepository(Category::class)->find($categoryId)) {
return;
}
// Create the new category
$category = new Category();
$category->setName($categoryId);
$this->em->persist($category);
$this->em->flush();
$data['category'] = $category->getId();
$event->setData($data);
});
No, there is not.
You should implement this manually by either:
using the select2 events to create the new choice via ajax
catching the posted options before validating the form, and add it to the options list
When creating a custom field in Symfony, there is a method we define getParent
We define our class by extending from AbstractType class, then return a parent type using getParent method. instead of extending from parent class.
I want to know the philosophy behind this approach.
Is it possible to define my custom type like:
class ImageType extends FileType
{
public function getName()
{
return 'image';
}
}
instead of this :
class ImageType extends AbstractType
{
public function getParent()
{
return 'file';
}
public function getName()
{
return 'image';
}
}
If can, then what is the difference between these two approach?
Thanks!
There are two main differences:
The first one is about FormTypeExtensions. These extensions modify certain form types (e.g: they could change/add some default options, or even add a field).
Using the first approach (e.g. Inheritance), all extensions for the FileType type will be applied to the ImageType, but using the second approach (e.g. getParent), they won't, thus you have more control over your structure.
The second difference is about modifying the behaviour of the parent form inside child form, using buildForm and buildView.
Using the first approach (e.g. Inheritance), will override the base class's methods if you provide them in child, but the second approach (e.g. getParent) will add the child's logic to that of parent.
Consider the following example:
// FileType
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('name', 'text');
}
// ImageType
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('email', 'email');
}
Inheritance:
form fields: [email]
getParent
form fields: [name] [email]
No, you need to extend using AbstractType. This is used for displaying and building a form and is not a simple entity that you are extending. The base type, FileType in your case, relates to an file with specific methods and you will be allowed to easily override them but extending through AbstractType and can add new fields. If you extended FileType, I do not think Symfony2 would load any new functions properly.
I think the first method is more compact and would like to use it, but I think this would cause problems if you are adjusting the buildView or setDefaultOptions, or adding another method that was not part of the base type.
When a form doesn't validate, I need to access the submitted data inside a Form Class in order I can set some options in a custom field.
I have tried with
$data = $builder->getForm()->getData();
$data = $builder->getData();
but $data has the empty object. So... what is the correct form to access the submitted data by the user after validation error in the form class?
Thanks
The problem is you're trying to access submitted data when it has not be handled yet. Basically, when you are in a builder (buildForm for the abstract types), you are building your form structure. It has nothing to do with form submission/binding. This is why you get the initial data when you call $builder->getData() because it only know the initial data at this state.
Knowing that the form component allows you to access the submitted data via events. You can attach a listener to your builder and rely on one of the *_submit event. The FormEvent class will given you the submitted data with $event->getData().
See this doc for more information: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
Look into $options variable (var_dump it)
As I remeber you are looking for
$options['data']
Using Form Events.
For those who wonder how Form Events are used.
Here is an example where you can modify the form after the user has tapped the submit button.
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
// ...
/* Listener to order to set a price if it does not exist yet */
$builder->get('price')->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
// modify it as you wish
$event->setData($data);
});
The FormEvents::PRE_SUBMIT event is dispatched at the beginning of the
Form::submit() method.
If needed, here is an example where you can modify the form price before you display it.
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
// ...
/* Listener to order to set a price if it does not exist yet */
$builder->get('price')->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$data = $event->getData();
if (null === $data) { $data = '0.00'; }
$event->setData($data);
});
The FormEvents::PRE_SET_DATA event is dispatched at the beginning of
the Form::setData() method.
I'm trying to get and manipulate the actual object related to a ImageAdmin class in SonataAdmin (using Symfony 2.3). This works fine when the ImageAdmin class is the only one being used. But when ImageAdmin is embedded in another Admin it goes horribly wrong.
Here's what works when you don't have embedded Admins:
class ImageAdmin extends Admin {
protected $baseRoutePattern = 'image';
protected function configureFormFields(FormMapper $formMapper) {
$subject = $this->getSubject();
}
}
But when you embed ImageAdmin in ParentAdmin using this:
class PageAdmin extends Admin {
protected function configureFormFields(FormMapper $formMapper) {
$formMapper->add('image1', 'sonata_type_admin');
}
}
Then when you're editing a Parent item with id 10 and call getSubject() in ImageAdmin you get the Image with id 10!
In other words getSubject() extracts the id from the URL then calls $this->getModelManager()->find($this->getClass(), $id);, which cross-references the Parent id and the Image id. Oops!
So... what I want to do is be able to get hold of the actual object that is being rendered/edited in the current ImageAdmin instance, whether it's being edited directly or via an embedded form, and then be able to do things with it.
Maybe getSubject() is the wrong tree to be barking up, but I note that $this->getCurrentChild() returns false when called from ImageAdmin::configureFormFields(), even when that ImageAdmin is embedded using the sonata_type_admin field type. I'm quite confused...
Anyway, I hope it is possible to get hold of the object in some obvious way that I've overlooked and somebody here can help enlighten me!
Thanks to Tautrimas for some ideas, but I managed to figure out an answer to this:
In ImageAdmin set this:
protected function configureFormFields(FormMapper $formMapper)
{
if($this->hasParentFieldDescription()) { // this Admin is embedded
$getter = 'get' . $this->getParentFieldDescription()->getFieldName();
$parent = $this->getParentFieldDescription()->getAdmin()->getSubject();
if ($parent) {
$image = $parent->$getter();
} else {
$image = null;
}
} else { // this Admin is not embedded
$image = $this->getSubject();
}
// You can then do things with the $image, like show a thumbnail in the help:
$fileFieldOptions = array('required' => false);
if ($image && ($webPath = $image->getWebPath())) {
$fileFieldOptions['help'] = '<img src="'.$webPath.'" class="admin-preview" />';
}
$formMapper
->add('file', 'file', $fileFieldOptions)
;
}
I'll post this in the upcoming SonataAdmin cookbook soon!
https://github.com/sonata-project/SonataAdminBundle/issues/1546
caponica's solution is working only on oneToOne relations, am I right? In my oneToMany case , this: $parent->$getter() returns a collection, and I don't know how to identify the current subject.
I've found this bug report:
https://github.com/sonata-project/SonataAdminBundle/issues/1568, which contains a fix for this, but it is still open, so I hope they merge it soon:(
Edit
With some research there is a temporary fix for this: Fixed getting wrong subject in sonata_type_collection
In short:
create a class and copypaste the content of this file: AdminType
then add this to your services.yml, and change the class namespace to you new class namespace:
services:
sonata.admin.form.type.admin:
class: ACME\AdminBundle\Form\Type\AdminType
tags:
- { name: form.type, alias: sonata_type_admin }
It still has a bug though:
also fix doesn't work when enabled cascade_validation in the parent docment and embedded form has errors
Can you try $this->getForm()->getViewData(); within your ImageAdmin? This should get you the correct child entity.
I tried all these solutions, but none proved to work.
So, I worked to find a solution. My solution is based on caponica's solution, but work on oneToMany case. Tha solution I found is a workaround, but works good.
It's working using the session.
public function getCurrentObjectFromCollection($adminChild)
{
$getter = 'get' . $adminChild->getParentFieldDescription()
->getFieldName();
$parent = $adminChild->getParentFieldDescription()
->getAdmin()
->getSubject();
$collection = $parent->$getter();
$session = $adminChild->getRequest()->getSession();
$number = 0;
if ($session->get('adminCollection')) {
$number = $session->get('adminCollection');
$session->remove('adminCollection');
}
else {
$session->set('adminCollection', 1 - $number);
}
return $collection[$number];
}
And you get the correct object in the admin by:
$object = $this->getCurrentObjectFromCollection($this)
So, when the parent needs to show the list of child admins, each child admin will run this function and will update the session parameter. When all the elements have been taken, the session parameter is deleted.
This code is made for lists with only 2 elements, but can be updated for any number of elements.
Hope this helps somebody :)
I had same problem and i am able to do this through "Custom Form Type Extension" for which documentation is given on the link "http://symfony.com/doc/current/cookbook/form/create_form_type_extension.html" .
It is the perfect solution ..
I have been looking through the documentation, and I can't seem to find a way to do this. I know I can use headScript to add style sheets to individual views, but I would like to add a style sheet to all actions in a controller.
Has anyone done this? I am sure it is a simple task.
Thanks
What you need to do is hook into the dispatch event and, based on the type of controller that was dispatched, set the appropriate layout (recommended). You could also directly modify the view and add the required assets.
This can be achieved by using the following code in your Module class:
<?php
namespace App;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$event->getApplication()->getEventManager()->getSharedManager()->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, function (MvcEvent $event)
{
$application = $event->getApplication();
$services = $application->getServiceManager();
$view = $services->get('ViewRenderer');
$controller = $event->getTarget();
if ($controller instanceof \App\Controller\Entry)
{
$controller->layout('layout/app/entry');
// -- OR --
$view->headStyle()->appendStyle('body{background:red}');
}
}, 100);
}
}
I hope this answers your question!