I have following entity field:
/**
* #Assert\Regex(
* pattern = "/^d+\.(jpg|png|gif)$/",
* htmlPattern = "/^d+\.(jpg|png|gif)$/"
* )
**/
protected $src;
The form is created by something like this:
$builder
->add('src', TextareaType::class, array( //neither is TextType::class working
'attr' => array('class' => 'mysrc'),
)); //pattern will not be rendered
The problem is, as soon as I provide the field type class TextareaType::class the regex pattern isn't rendered as constraint to the form. Or in other words: The regex pattern is only rendered, if the field type is guessed:
$builder
->add('src', null, array(
'attr' => array('class' => 'mysrc'),
)); //pattern will be rendered
Any workaround? I want to have the regex patterns in one place, i.e. in my entity, not in a controller or a form.
Yepp, this is how it should work. Not only the field type is guessed, but some options too and if the type is not guessed the options neither.
BTW the textarea element does not support the pattern attribute, but if you want to you can add one: 'attr' => array('pattern' => '/jsregex/'), you should use a different pattern for the client side than the server side anyway. If you want to give the pattern in one place use a constant.
Maybe this one could help you:
FormType
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints\Regex;
use YourBundle\Entity\YourEntity;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function getRegexPattern($property)
{
$pattern = null;
$metadata = $this->validator->getMetadataFor(new YourEntity());
$propertyMetadata = $metadata->getPropertyMetadata($property);
$constraints = $propertyMetadata[0]->constraints;
foreach($constraints as $constraint) {
if ($constraint instanceof Regex) {
$pattern = $constraint->pattern;
}
}
return $pattern;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('src', TextType::class, array(
'attr' => array(
'class' => 'mysrc',
'pattern' => $this->getRegexPattern('src')
),
));
}
services.yml (Needed to inject validator in type)
services:
app.form.type.your_entity_type:
class: YourBundle\Form\YourEntityType
arguments:
validator: "#validator"
tags:
- { name: form.type }
Rendered
<input type="text" pattern="/^d+\.(jpg|png|gif)$/" class="src"/>
Like this, even if the TextType in manually set, you get the html validation through pattern retrieved from the constraints of the property.
Related
We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html
I am trying to order an entity form field witch is translated.
I am using the symfony translation tool, so i can't order values with a SQL statement.
Is there a way to sort values after there are loaded and translated ?
Maybe using a form event ?
$builder
->add('country', 'entity',
array(
'class' => 'MyBundle:Country',
'translation_domain' => 'countries',
'property' => 'name',
'empty_value' => '---',
)
)
I found the solution to sort my field values in my Form Type.
We have to use the finishView() method which is called when the form view is created :
<?php
namespace My\Namespace\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\Translation\Translator;
class MyFormType extends AbstractType
{
protected $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
// Order translated countries
$collator = new \Collator($this->translator->getLocale());
usort(
$view->children['country']->vars['choices'],
function ($a, $b) use ($collator) {
return $collator->compare(
$this->translator->trans($a->label, array(), 'countries'),
$this->translator->trans($b->label, array(), 'countries')
);
}
);
}
// ...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country', 'entity',
array(
'class' => 'MyBundle:Country',
'translation_domain' => 'countries',
'property' => 'name',
'empty_value' => '---',
)
)
;
}
}
OLD ANSWER
I found a solution for my problem, I can sort them in my controller after creating the view :
$fview = $form->createView();
usort(
$fview->children['country']->vars['choices'],
function($a, $b) use ($translator){
return strcoll($translator->trans($a->label, array(), 'countries'), $translator->trans($b->label, array(), 'countries'));
}
);
Maybe I can do that in a better way ?
Originally I wished to do directly in my form builder instead of adding extra code in controllers where I use this form.
I think it's impossible. You need to use PHP sorting, but if you use Symfony Form Type, I would advise to sort it with JavaScript after page is loaded.
If your countries are in an array, just use the sort() function, with the SORT_STRING flag. You will do some gymnastic to have it in my opinion.
Check this doc : http://php.net/manual/fr/function.sort.php
My scenario is,
I need to create a entity(module) with display order and the module is under a topic entity and association made correctly. On form load the topic dropdown and display order dropdown will be blank along the module name. When selecting topic the display order will fill with options via ajax/js. Display order will be 1 to a number that will be the total modules under the specific topic+1 . The upcoming display order will be selected automatically. And that's working perfectly. But my issue is about the display order validation after submit. Its saying 'This value is not valid'. I understands this is due to not giving 'choices' as array in form type, but this case i cant give as static in form type. Please help anyone knows a solution.
class ModuleType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('topic', EntityType::class, [
'class' => 'AppBundle:Topic',
'choice_label' => 'name',
'placeholder' => 'Choose a Topic'
])
->add('name')
->add('description', TextareaType::class)
->add('displayOrder', ChoiceType::class)
->add('save', SubmitType::class, [
'attr' => ['class' => 'form-control button btn-sm nomargin']
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Module'
));
}
}
Try to use the Event Listener.
In your case for exemple:
// TopicType.php
private function addEventListener(FormBuilderInterface $builder)
{
// this function permit to valid values of topics
$annonymFunction = function(FormInterface $form, $diplayOrder) {
$entities = $this->container->get('doctrine.orm.default_entity_manager')
->getRepository('YourBundle:Topic')
->findAll();
if ($entities) {
$topics = array();
foreach($topics as $topic) {
$topics[$topic->getName()] = $topic->getName();
}
}
else $topics = null;
$form->add('topic', EntityType::class, array(
'attr' => array('class' => 'topic'),
'choices' => $topics));
};
$builder
->get('displayOrder')
->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) use ($annonymFunction) {
$annonymFunction($event->getForm()->getParent(), $event->getForm()->getData());
});
}
Hope to help.
I am working with two ManyToMany related entities, namely category and tag.
The entity Tag(relevant details):
/**
*
* #var string
*
* #ORM\Column(name="tagname", type="string")
*/
protected $tagname;
/**
* #ORM\ManyToMany(targetEntity="Category", mappedBy="tags")
*/
protected $categories;
The entity Category(relevant details):
/**
*
* #var string
*
* #ORM\Column(name="CategoryName", type="string",length=200)
*/
protected $categoryname;
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="categories")
*/
protected $tags;
I have a form with a select-input(CategoryType) and a multiple select-input(TagType) fields. Both the fields are EntityType fields. The TagType is embedded inside the CatgoryType.
For this I am not able to utilise the cascade=persist functionality and am adding the submitted tags manually inside my controller. On submission the form data gets persisted in the database without any issues.
The problem is, after submission, when I fetch the submitted category(and the associated tags) in my controller, and pass it to the form, I get this error - Unable to transform value for property path "tagname": Expected a Doctrine\Common\Collections\Collection object.
The var_dump result of the fetched category object(var_dump($category->getTags()->getValues());) gives me an array of the associated Tag objects, with the property protected 'tagname' => string 'tag1'.
From what I understand Interface Collection is quite similar to a php array and My guess is that the tagname field expects all the tagnames in an ArrayCollection or Collection object format. I am not sure whether what is the specific difference.
However I am still clueless how do I pass the already persisted category object in my form.
Here are the categoryname and tags field in the CategoryType:
$builder->add('categoryname', EntityType::class, array(
'class' => 'AppBundle:Category',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.id', 'ASC');
},
'choice_label' => 'categoryname',
'expanded' => false,
'multiple' => false,
'label' => 'Choose Category',
));
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
));
Here is the embedded tagname field in the TagType:
$builder->add('tagname', EntityType::class, array(
'class' => 'AppBundle:Tag',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('t')
->orderBy('t.id', 'ASC');
},
'choice_label' => 'tagname',
'expanded' => false,
'multiple' => true,
'label' => 'Choose Tags',
));
Any ideas?
Try to rid off 'multiple' => true in embedded form. This worked for me.
I've had a similar problem where my EntityType form element gave me this error message:
Unable to reverse value for property path 'my_property' Expected an array.
Setting multiple to false would make the implementation useless, so a better option for me was to set the empty_data to an empty array:
"empty_data" => [],
In your case you might be able to solve the problem by setting the empty_data to a Collection, something like this will probably work:
"empty_data" => new ArrayCollection,
Here is how you do it (from source: https://www.youtube.com/watch?v=NNCpj4otnrc):
***And here is a text version of this image #1, the reason I did not add text because it was hard to read when the format is not proper!
<?php
namespace App\Form;
use App\Entity\Post;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'attr' => [
'placeholder' => 'Etner the title here',
'class' => 'custom_class'
]
])
->add('description', TextareaType::class, [
'attr' => [
'placeholder' => 'Enter teh description here',
]
])
->add('category', EntityType::class, [
'class' => 'App\Entity\Category'
])
->add('save', SubmitType::class, [
'attr' => [
'class' => 'btn btn-success'
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}
***And here is a text version of this image #2, the reason I did not add text because it was hard to read when the format is not proper!
class FormController extends AbstractController
{
/**
* #Route("/form", name="form")
* #param Request $request
* #return Response
*/
public function index(Request $request)
{
$post = new Post(); // exit('this');
/*$post->setTitle('welcome');
$post->setDescription('description here');*/
$form = $this->createForm(PostType::class, $post, [
'action' => $this->generateUrl('form'),
'method' => 'POST'
]);
// handle the request
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// var_dump($post); die();
// save to database
$em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
}
return $this->render('form/index.html.twig', [
'postaform' => $form->createView()
]);
}
My assumption was wrong, I need to either make tagname field an array or create a ManyToOne or ManyToMany relationship with any other entity so it can be an arraycollection. Only then it is possible to use tagname as a multiple select field.
/**
*
* #var array
*
* #ORM\Column(name="tagname", type="array")
*/
protected $tagname;
or
/**
* #ORM\ManyToMany(targetEntity="SOME_ENTITY", mappedBy="SOME_PROPERTY")
*/
protected $tagname;
or
/**
* #ORM\ManyToOne(targetEntity="SOME_ENTITY", mappedBy="SOME_PROPERTY")
*/
protected $tagname;
The exception thrown is pretty explicit, and the problem is probably here:
$builder->add('tagname', EntityType::class, array()
According to your Tag entity, Tag::$tagname is not an entity collection, it's a string. You should add the property with
$builder->add('tagname', TextType::class, array()
shouldn't you ?
Problem description
I need to define object-related field (like sonata_type_model_list) inside sonata_type_immutable_array form type definition:
$formMapper->add('options', 'sonata_type_immutable_array', array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
)
)
)
This is not working, here is error message:
Impossible to access an attribute ("associationadmin") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
What I found about this problem
I tryed to find any information about using sonata_type_model_list inside sonata_type_immutable_array, but there is very little information.
This (https://github.com/a2lix/TranslationFormBundle/issues/155) topic helped me a bit, but doing all in the same manner I've got another error:
Impossible to invoke a method ("id") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
So I totally failed in uderstanding what I have to do.
My context
-- I have Doctrine ORM Mapped class called CmsLink, it defines object to which 'linkPath' field relates.
-- I have admin class for CmsLink class, it has very basic configuration:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('technicalAlias')
->add('url')
;
}
-- I have Doctrine ORM Mapped class called CmsMenuItem, it defines object and 'options' filed which persists data managed by sonata_type_immutable_array form type, field type is json_array:
/**
* #var string
*
* #ORM\Column(name="options", type="json_array", nullable=true)
*/
private $options;
-- And finally I have admin class for CmsMenuItem class, here is the key code piece:
$linkAdmin = $this->configurationPool->getAdminByClass("Argon\\CMSBundle\\Entity\\CmsLink");
$formMapper
->add('options', 'sonata_type_immutable_array',
array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
),
array('description', 'textarea', array()),
array('image', 'sonata_media_type',
array(
'provider' => 'sonata.media.provider.image',
'context' => 'pages_static',
'required'=>false,
)
)
)
)
);
Question goals
Find out what I need to do to bring life into this idea?
Get general information and understanding abot how to include object-related field types into sonata_type_immutable_array
I've just come across this problem, and solved it with a custom type and data transformers.
Here's the rough outline, though you need to tailor it to your problem.
Custom Type
class YourImmutableArrayType extends ImmutableArrayType
{
/**
* #var YourSettingsObjectTransformer
*/
private $transformer;
public function __construct(YourSettingsObjectTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer($this->transformer);
}
public function getName()
{
return 'your_type_name';
}
public function getParent()
{
return 'sonata_type_immutable_array';
}
}
Custom model transformer
class NewsListingSettingsTransformer implements DataTransformerInterface
{
public function __construct(ObjectManager $manager)
{
// You'll need the $manager to lookup your objects later
}
public function reverseTransform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert your objects in array to IDs
return $value;
}
public function transform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert ids embedded in your array to objects,
// or ArrayCollection containing objects
return $value;
}
}
Building form in admin class
$formMapper->add('settings', 'your_type_name', array(
'keys' => array(
array(
'collectionOfObjects',
'sonata_type_model',
array(
'class' => YourObject::class,
'multiple' => true,
'model_manager' => $this->yourObjectAdmin->getModelManager()
)
)
)
));
}
Again, it's a rough outline, so tweak it to your needs.