How can I use the current login user in a symfony form? - forms

I trying to do a form that use the login user to fill a EntityType and use it like the 'author'
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('Title')
>add('Comment')
->add('Author', EntityType::class, [
'class' => User::class,
'choice_label' => ['name']
]);
}
I tried to do it but I can't find the way to do it

This can be done on the individual action basis, from your controller as mentioned by SubCore. However, if you want it always work automatically from anywhere you persist the entity use an event listener.
Here is one I used in a Symfony 4.4.8 project that sets the current user in an entity's createdBy/editedBy field:
namespace App\EventListener;
use App\Application\Sonata\UserBundle\Entity\User;
use App\Entity\CMSPage;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Security;
class CMSPageListener
{
private $security;
public function __construct(Security $security)
{
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
// the entity listener methods receive two arguments:
// the entity instance and the lifecycle event
public function preUpdate(CMSPage $page, LifecycleEventArgs $event)
{
// returns User object or null if not authenticated
$user = $this->fetchCurrentUser($event);
$page
->setEditedBy($user)
->setUpdatedAt(new \DateTime())
;
}
public function prePersist(CMSPage $page, LifecycleEventArgs $event)
{
$now = new \DateTime();
if (null === $page->getCreatedBy()) {
$page->setCreatedBy($this->fetchCurrentUser($event));
}
$page
->setCreatedAt($now)
->setUpdatedAt($now)
;
}
public function fetchCurrentUser(LifecycleEventArgs $event)
{
// returns User object or null if not authenticated
$coreUser = $this->security->getUser();
/** #var User $user */
$user = $event->getObjectManager()->getRepository(User::class)->findOneBy([
'username' => $coreUser->getUsername(),
])
;
return $user;
}
}
And here is the config/services.yaml
App\EventListener\CMSPageListener:
tags:
-
# these are the basic options that define the entity listener
name: 'doctrine.orm.entity_listener'
event: 'preUpdate'
entity: 'App\Entity\CMSPage'
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
# you can also associate an entity listener to a specific entity manager
#entity_manager: 'custom'
# by default, Symfony looks for a method called after the event (e.g. postUpdate())
# if it doesn't exist, it tries to execute the '__invoke()' method, but you can
# configure a custom method name with the 'method' option
#method: 'checkUserChanges'
- { name: 'doctrine.orm.entity_listener', event: 'prePersist', entity: 'App\Entity\CMSPage', lazy: true }

Related

Symfony Forms: Adding a CallbackTransformer to a field that is added in an EventListener

I have a somewhat complex form and am struggling with adding a ModelTransformer to a dynamically added field.
First I have a basic form with some fields and one CollectionType field that includes a custom Type:
class FilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// adding some other fields here ...
$builder->add('conditions', Type\CollectionType::class, [
'entry_type' => FilterRowType::class,
'allow_add' => true,
'prototype' => true,
'allow_delete' => true,
'entry_options' => ['label' => false],
]);
}
}
The FilterRowType consists of several fields that are depending on each other.
First the user has to select an option from a dropdown and then another field is added whose type and options depend on the selected value of the first field.
The second field could be TextType or NumberType or even ChoiceType with its choices again depending on the first field.
Finally I need to add a CallbackTransformer to this second field.
So here is what I currently have (widely stripped of stuff I think is not important for this question):
class FilterRowType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('attribute', Type\ChoiceType::class, [
'choices' => $this->getAttributeChoices(),
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
}
public function addDynamicInputs(FormEvent $event, FormBuilderInterface $builder)
{
$form = $event->getForm();
$data = $event->getData();
// adding some other fields ...
$valueConfig = $this->getValueConfig($data['attribute']);
$form->add('value', $valueConfig['type'], $valueConfig['options']);
$valueConfig['options']['auto_initialize'] = false;
$form->add(
$builder->create('value', $valueConfig['type'], $valueConfig['options'])
->addModelTransformer($this->getCallbackTransformer ())
->getForm()
);
}
}
And this is actually working ! :)
BUT:
As you might already have spotted I am actually adding the 'value' field twice here.
This happened by accident as I added the CallbackTransformer later and forgot to delete the original line.
The problem is that if I now remove the original line $form->add('value', $valueConfig['type'], $valueConfig['options']); I run into an exception:
Neither the property "value" nor one of the methods "value()", "getvalue()"/"isvalue()"/"hasvalue()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
Probably because I set $valueConfig['options']['auto_initialize'] = false; for the new creation of the field?
But if I remove that line I run into a different error:
Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "value"
Of course I could leave everything as it is with adding the 'value' field twice.
But that seems a very fishy solution to me and I am afraid that it might have some unforeseen consequences even if currently everything seems to work fine.
So can maybe someone with more insight into symfony forms enlighten me?
Are there possible problems with my 'solution' ?
Is there a better/proper way of doing what I am trying to do?
I had the same case today. Here is what I did :
Create an extension :
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ModelTransformerExtension extends AbstractTypeExtension {
public static function getExtendedTypes(): iterable {
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
if (isset($options['model_transformer'])) {
$builder->addModelTransformer($options['model_transformer']);
}
}
public function configureOptions(OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(array('model_transformer' => null));
}
}
Use it in your form field options :
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$form->add('fieldName', $TextType::class, [
'model_transformer' => // Your transformer here
]);
});

Array-type property and Form CollectionType with data transformer

I have an entity with a property that is set as an array
/**
* #ORM\Column(type="array")
*/
private $labels = [];
this array of data stores translations of a label like
[
'en' => 'foo-in-English',
'de' => 'foo-in-German',
'ru' => 'foo-in-Russian'
]
I have a Form with the type set for the labels like:
$builder
->add('labels', CollectionType::class);
Note that the entry_type defaults (properly) to TextType here. Left as is, the template would be displayed with text fields, like:
Labels: en: _____FOO IN ENGLISH____
de: _____FOO IN GERMAN_____
ru: _____FOO IN RUSSIAN____
But, I would like the fields to be displayed with the actual language name and not the two-letter code as the label, so something like:
Labels: English: _____FOO IN ENGLISH____
German: _____FOO IN GERMAN_____
Russian: _____FOO IN RUSSIAN____
I also want to make sure that all my selected/supported languages are displayed - even if they currently have no value.
So, this seems like the proper place for a DataTransformer, but try as I might I could not get this concept to work within the Form class. It seems that attempting to transform the data of a collection type is more difficult (or impossible?) than a simpler type like text.
I've overcome this as a workaround by transforming the data within the controller before submitting it to the form and after processing the form before persistence. e.g.
$this->transformTranslations($fooEntity);
$form = $this->createForm(FooType::class, $fooEntity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$fooEntity = $form->getData();
$this->reverseTransformTranslations($fooEntity);
$this->getDoctrine()->getManager()->persist($fooEntity);
$this->getDoctrine()->getManager()->flush();
...
I'm wondering if anyone has a better method (like how to use normal data or model transformers). I can't seem to find much online about using data transformers with collection types. TIA!
I have not personally used a doctrine array value before, however
you can define a 'default' form class for each of your translation options like so:
AppBundle\Form\LanguageStringEditorType.php
class LanguageStringEditorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('en', TextareaType::class, ['label' => 'English'])
->add('de', TextareaType::class, ['label' => 'German'])
->add('ru', TextareaType::class, ['label' => 'Russian'])
;
}
}
If you keep the naming ('en', 'de' and 'ru') the same as your data array key names for example having an (doctrine) entity like this:
AppBundle\Entity\LanguageString.php
class LanguageString {
private $identifier;
private $translations; // this is the doctrine array type
// however I didn't feel like setting up a database for this
// test so I'm manually filling it see the next bit
... Getter and setter things ...
And create a type for that as well:
AppBundle\Form\LanguageStringType.php
class LanguageStringType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('identifier')
->add('translations', LanguageStringEditorType::class, ['label' => 'Translations'])
;
}
}
We can use this in the controller
$data = new LanguageString();
// fill some dummy content (not using database)..
$data->setIdentifier('hello_world');
$data->setTranslations([
'en' => 'Hello world!',
'de' => 'Hallo Welt!',
'ru' => 'Привет мир'
]);
$form = $this->createForm(LanguageStringType::class, $data);
return $this->render('default/index.html.twig', [
'form' => $form->createView()
]);
And the rest is done by magic, no transformers required. The data is placed in the form fields. And set to the entity when using the handleRequest. Just remember that the data key values are the same as the form builder names.
And as a bonus you have defined all your default language fields in the LanguageStringEditorType class, filled in or not.
So, I learned I needed to separate my two needs into different solutions. First I created a new form type to use instead of the text type I was using by default:
$builder
])
->add('labels', CollectionType::class, [
'entry_type' => TranslationType::class
])
This class is very simple and is only an extension of a regular TextType:
class TranslationType extends AbstractType
{
/**
* #var LocaleApiInterface
*/
private $localeApi;
/**
* TranslationType constructor.
* #param LocaleApiInterface $localeApi
*/
public function __construct(LocaleApiInterface $localeApi)
{
$this->localeApi = $localeApi;
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['label'] = array_search($view->vars['name'], $this->localeApi->getSupportedLocaleNames());
}
public function getParent()
{
return TextType::class;
}
}
This satisfied the labelling issue. Second, to ensure I had all the supported locales in my data, I used a FormEventListener:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$supportedLocales = $this->localeApi->getSupportedLocales();
$data = $event->getData();
$labels = $data['labels'];
foreach ($supportedLocales as $locale) {
if (!array_key_exists($locale, $labels)) {
$labels[$locale] = $labels['en'];
}
}
$data['labels'] = $labels;
$event->setData($data);
});
This adds the required keys to the data if they are not already present.

field array type in entity for form choice type field symfony

I would like to create a UserForm for create user in my system backend.
I use a entity with a 'role' field as type array
I want use a select choice field type Form with that entity field.
I use a transformer class system for convert data between Entity and form.
but I turn around in my head and nothing run correctly.
When I use options 'multiple' of choice type, my field display correctly but I don't want to display and select multiple value for this field.
I have Notice: Undefined offset: 0 error
or
I have ContextErrorException: Notice: Array to string conversion
Here few essential code :
UserForm class
$builder->add($builder->create('roles', 'choice', array(
'label' => 'I am:',
'mapped' => true,
'expanded' => false,
'multiple' => false,
'choices' => array(
'ROLE_NORMAL' => 'Standard',
'ROLE_VIP' => 'VIP',
)
))->addModelTransformer($transformer));
transformer Class
class StringToArrayTransformer implements DataTransformerInterface
{
public function transform($array)
{
return $array[0];
}
public function reverseTransform($string)
{
return array($string);
}
}
controller method
$user = new User(); //init entity
$form = $this->createForm(new UserForm(), $user);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($form);
$em->flush();
return $this->redirect($this->generateUrl('task_success'));
}
entity part
/**
* #ORM\Column(name="roles", type="array")
*/
protected $roles;
public function getRoles()
{
return $this->roles;
}
public function setRoles(array $roles)
{
$this->roles = $roles;
return $this;
}
My field roles entity must be a array for run correctly the security component Symfony
can you help me to understand why this field form refuse to display ?
I already readed others questions in same issue but there is anything that I don't understand because nothing help me to resolve my problem.
If you can help me with MY particular context...
Thank for support
because security symfony component integration
If you only need the "getRoles" method because of the interface you are implementing, it is simpler (and cleaner) to do the following:
Change the entities field again to role with type string
Rename your getter and setter to getRole() and setRole()
and add a getRoles method like this:
public function getRoles()
{
return array($this->role);
}
In your form type, change the field name to "role" and 'multiple' => false
Remove your model transformer
This should be the solution ;)

Symfony2 - Dynamic form choices - validation remove

I have a drop down form element. Initially it starts out empty but it is populated with values via javascript after the user has made some interactions. Thats all working ok. However when I submit it always returns a validation error This value is not valid..
If I add the items to the choices list in the form code it will validate OK however I am trying to populate it dynamically and pre adding the items to the choices list is not going to work.
The problem I think is because the form is validating against an empty list of items. I don't want it to validate against a list at all. I have set validation required to false. I switched the chocie type to text and that always passes validation.
This will only validate against empty rows or items added to choice list
$builder->add('verified_city', 'choice', array(
'required' => false
));
Similar question here that was not answered.
Validating dynamically loaded choices in Symfony 2
Say you don't know what all the available choices are. It could be loaded in from a external web source?
after much time messing around trying to find it. You basically need to add a PRE_BIND listener. You add some extra choices just before you bind the values ready for validation.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
public function buildForm(FormBuilderInterface $builder, array $options)
{
// .. create form code at the top
$ff = $builder->getFormFactory();
// function to add 'template' choice field dynamically
$func = function (FormEvent $e) use ($ff) {
$data = $e->getData();
$form = $e->getForm();
if ($form->has('verified_city')) {
$form->remove('verified_city');
}
// this helps determine what the list of available cities are that we can use
if ($data instanceof \Portal\PriceWatchBundle\Entity\PriceWatch) {
$country = ($data->getVerifiedCountry()) ? $data->getVerifiedCountry() : null;
}
else{
$country = $data['verified_country'];
}
// here u can populate choices in a manner u do it in loadChoices use your service in here
$choices = array('', '','Manchester' => 'Manchester', 'Leeds' => 'Leeds');
#if (/* some conditions etc */)
#{
# $choices = array('3' => '3', '4' => '4');
#}
$form->add($ff->createNamed('verified_city', 'choice', null, compact('choices')));
};
// Register the function above as EventListener on PreSet and PreBind
// This is called when form first init - not needed in this example
#$builder->addEventListener(FormEvents::PRE_SET_DATA, $func);
// called just before validation
$builder->addEventListener(FormEvents::PRE_BIND, $func);
}
The validation is handled by the Validator component: http://symfony.com/doc/current/book/validation.html.
The required option in the Form layer is used to control the HTML5 required attribute, so it won't change anything for you, and that is normal.
What you should do here is to configure the Validation layer according to the documentation linked above.
Found a better solution which I posted here: Disable backend validation for choice field in Symfony 2 Type
Old answer:
Just spent a few hours dealing with that problem. This choice - type is really annoying. My solution is similar to yours, maybe a little shorter. Of course it's a hack but what can you do...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('place', 'choice'); //don't validate that
//... more form fields
//before submit remove the field and set the submitted choice as
//"static" choices to make "ChoiceToValueTransformer" happy
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if ($form->has('place')) {
$form->remove('place');
}
$form->add('place', 'choice', array(
'choices' => array($data['place']=>'Whatever'),
));
});
}
Add this inside buildForm method in your form type class so that you can validate an input field value rather a choice from a select field value;
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
if ($form->has('verified_city')) {
$form->remove('verified_city');
$form->add(
'verified_city',
'text',
['required' => false]
)
}
}
);
Update in Validations.yml
Kindly update the Validation.yml file in the below format : setting the group names in the each field
password:
- NotBlank: { message: Please enter password ,groups: [Default]}
Update in Form Type
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'RegistrationBundle\Entity\sf_members',
'validation_groups' => function(FormInterface $form){
$data = $form->getData();
$member_id = $data->getMemberId();
// Block of code;
// starts Here :
if( condition == 'edit profile') {
return array('edit');
} else
{
return array('Default');
}
},
Update in Entity
/**
* #var string
*
* #ORM\Column(name="password", type="text")
* #Assert\Regex(
* pattern="/(?i)^(?=.[a-zA-Z])(?=.\d).{8,}$/",
* match=true,
* message="Your password must be at least 8 characters, including at least one number and one letter",
* groups={"Default","edit"}
* )
*/
private $password;

Proper way to update class object in db using symfony2 + doctrine + form?

I have a simple class:
class Type
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=15)
*/
private $name;
...
}
And have a some 'type' objects in database.
So, if i want to change one of them, i create new controller rule (like /types/edit/{id}) and new action:
public function typesEditViewAction($id)
{
...
$editedType = new Type();
$form = $this->createFormBuilder($editedType)
->add('name', 'text')
->add('id', 'hidden', array('data' => $id))
->getForm();
// send form to twig template
...
}
After that, i create another controller rule (like /types/do_edit) and action:
public function typesEditAction(Request $request)
{
...
$editedType = new Type();
$form = $this->createFormBuilder($editedType)
->add('name', 'text')
->add('id', 'hidden')
->getForm();
$form->bind($request); // <--- ERROR THERE !!!
// change 'type' object in db
...
}
And i found a small problem there.
Сlass 'Type' doesn't have аuto-generated setter setId() and on binding i got error.
Neither the property "id" nor one of the methods "setId()", "__set()" or "__call()" exist and have public access in class "Lan\CsmBundle\Entity\Type".
Now, i remove 'id' field from symfony2 form object ($form) and transmit it manually to template.
At second controller's action i have $form object and 'id'-field apart.
I don't know a 'proper'-way for doing that (updating 'type' class). Please help.
Symfony has an integrated ParamConverter which automatically fetches your entity from database and throws an Exception ( which you can catch in a listener ) if the entity is not found.
You can easily handle GET and POST requests in one controller method.
make sure you have the public getters and setters for your properties in your entity.
I added annotations to make the routing clearer and still have a working example.
use Vendor\YourBundle\Entity\Type;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
// ...
/**
* #Route("/edit/{id}", requirements={"id" = "\d+"})
* #Method({"GET", "POST"})
*/
public function editAction(Request $request, Type $type)
{
$form = $this->createFormBuilder($type)
->add('name', 'text')
->add('id', 'hidden')
->getForm()
;
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getEntityManager();
$em->flush(); // entity is already persisted and managed by doctrine.
// return success response
}
}
// return the form ( will include the errors if validation failed )
}
I strongly suggest you should create a form type to further simplify your controller.
For anyone else stumbling on this where you added the ID field to your FormType because the frontend needed it you can just set the ID column to "not-mapped" like so:
->add('my_field', 'hidden', ['mapped'=>false])
and it prevents the ID value trying to get used by the form processing method.