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

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.

Related

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

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 }

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.

Symfony2 form setting, unsetting associations

I have Company and Number entity which are related
/**
* #var Comapany
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="numbers", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="company", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
* #Assert\NotBlank(groups={"client"})
* #Assert\Valid()
*/
private $company;
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
I have created a form for creating and updating Company entity. This form should allow to set Number entities to it as well as unset them. This is how it looks rendered
And this is how it looks in code:
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
The problem is when creating new Company record, the form assigns Number entities, but the Number entities have property "company" which doesn't get assigned and so no relation is made. I have worked around this with form events:
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
foreach ($event->getData()->getNumbers() as $number) {
$number->setCompany($event->getData());
}
});
Which works for creating record, however when updating I have another issue, since I remove Number associations I have no access to them and thus can't update them in database. I could again select all Number entities assigned to form, and then filter out which were assigned to company and which were not and then manually update them, but this feels dirty and I would like to work it out in a clean way.
Finally found solution, as is turns out it's quite well documented:
http://symfony.com/doc/current/cookbook/form/form_collections.html
The changes I Had to make was:
1.Add by_reference property to entity form field: More information on that with an example: http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-new-tags-with-the-prototype
Basically what I figured that without this options symfony2 form uses own means of adding associations, and with this option set it calls methods "addNumber" and "removeNumber" inside Entity in which I had to manually add inverse side "number" association which goes to 2nd change I had to make.
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'by_reference' => false, //
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
2.I had explicitly set Inverse side association to owning side by calling method setComapany($this) from owning (Company Entity) side.
/**
* Add numbers
*
* #param \AppBundle\Entity\Number $numbers
* #return Company
*/
public function addNumber(\AppBundle\Entity\Number $numbers)
{
$numbers->setCompany($this); //!Important manually set association
$this->numbers[] = $numbers;
return $this;
}
These 2 changes are enough to make form automatically add associations. However with removing associations there's a little bit more.
3.Change I had to make to correctly unset associations was inside controller action itself: I had to save currently set associations inside new ArrayCollection variable, and after form validation manually go through each item in that collection checking if it exists after form was validated. Important note:
I had also manually unset inverse side association to owning side by calling:
"$number->setCompany(null);"
public function editAction(Request $request, Company $company)
{
$originalNumbers = new ArrayCollection();
foreach ($company->getNumbers() as $number) {
$originalNumbers->add($number);
}
$form = $this->createForm(new CompanyType(), $company);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($originalNumbers as $number) {
if (false === $company->getNumbers()->contains($number)) {
$company->getNumbers()->removeElement($number);
$number->setCompany(null); //!Important manually unset association
}
}
$em->persist($company);
$em->flush();
return $this->redirectToRoute('companies');
}
return $this->render('AppBundle:Company:form.html.twig', [
'form' => $form->createView()
]);
}
All of these steps are required to make this kind of logic function properly, luckily I was able to really good documentation for that.
PS. Note that I call
$em->persist($company);
$em->flush();
without persisting each "Number" Entity iterated inside loop which you can be seen in given Symfony2 documentation example, and which in this case would look like this:
$company->getNumbers()->removeElement($number);
$number->setCompany(null);
$em->persist($number);
This is because I setup Cascading Relations options inside my Entity class
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
My advise for anyone struggling with this is to read whole http://symfony.com/doc/current/cookbook/form/form_collections.html thoroughly especially sections marked by special signs ✎✚❗☀💡

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;