Entity (with constraints) + createFormBuilder (with extra fields) fail - forms

My User entity has few properties:
private $id;
/**
Assert\Email
*/
private $email;
/**
* #Assert\Length( min=6, minMessage="Password is too short (min 6 symbols)" )
*/
private $password;
I'm trying to create a Form for changing password:
createFormBuilder(null, ??? data-class=UserEntity ??? )
->add('YES_CHANGE_PASS',CheckboxType::class,['required'=>false])
->add('old_password',PasswordType::class,['required'=>false])
->add('new_password', RepeatedType::class,['required'=>false])
Problem: if I set the 'data-class=UserEntity::class' to this Form, it stops working (of course, because my Entity doesn't have OLD_PASSWORD\NEW_PASSWORD property). If I don't set 'data-class', the Form obviously won't inherit any constraints (like password #Assert\Length(min=6) constraint)
Solution#1 I dont like: hardcode all the needed constraints right in the createFormBuilder function. I dont like this way because if one day I want to change the Password minLength, I'll have to run through the whole project searching for such forms to edit this
Solution#2 madness: add all the extra fields to my Entity, so it will let me use the 'data-class:UserEntity'... and constraints...and validate... but it's obviously a sick solution
Any hints, please?

Change your form to this :
$form=$this->createFormBuilder()
->add('YES_CHANGE_PASS', CheckboxType::class, array(
'mapped'=>false,
'required'=>false,
))
->add('old_password', PasswordType::class, array(
'mapped'=>false,
'required'=>false,
))
->add('new_password', RepeatedType::class, array(
'mapped'=>false,
'required'=>false,
));
Then, when the form is submitted :
if($form->get('YES_CHANGE_PASS')->getData()) { // <== Used asked to change password?
if($passwordEncoder->isPasswordValid($this->getUser(), $form->get('YES_CHANGE_PASS')->getData())) { // <== Check if old password is valid
$form->get('new_password')->getData(); // <== Do what you need with the new password.
}
}

Related

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 ✎✚❗☀💡

Repository/controller: How can I force TYPO3 to load the field "sorting"?

In a controller/template I'd like to have access to the field sorting of an entity.
I've tried to access it like:
$category->getSorting();
But it fails, as the method does not exist. When I dump the entity, all those meta fields, like hidden, starttime etc. aren't listed at all.
How can I tell TYPO3 to load those fields along with the other fields of the entitiy?
Since you are in Extbase context, you have to add the property to your model or (if you use the model of another extension) extend it and add the property. In both cases a getter and a setter method is needed if you want to access and edit the properties value:
/**
* #var integer
*/
protected $sorting;
public function setSorting($sorting) {
$this->sorting = $sorting;
}
public function getSorting() {
return $this->sorting;
}
Make sure you have that field configured in the TCA as well:
...
'columns' => array(
'sorting' => array(
'label' => 'sorting',
'config' => array(
'type' => 'passthrough'
)
),
...
After this you should be able to access the sorting property.

Zf2 multicheckbox at least one element is always required

I can't validate a zf2 form with multicheckbox because at least one checkbox is always required.
I found a lot of reference to this issue (for example here - https://github.com/zendframework/zf2/issues/4845), but i didn't found a solution for this.
Does anybody know how to solve this problem ?
UPDATE: I use a doctrine 2 objectmulticheckbox which is extended from zf2 multichechbox. As is commented below the override of getInputFilterSpecification method, will solve the problem with form validation, but the values will still remain in database (values populated by objectmulticheckbox).
I found a seemingly easier way to get around this issue by setting the input filter 'required' to false inside the controller, after the form is instantiated.
<?php
$form = new CampaignForm($multiCheckboxOptions); // Setting up checkbox in form class
$form->getInputFilter()->get('my_multi_checkbox')->setRequired(false);
?>
You can override the getInputFilterSpecification function on your form to set the field to not be required. For example:
public function getInputFilterSpecification() {
return array(
[...]
'the-multi-checkbox-field' => array(
'required' => false,
),
[...]
);
}
Ok i did a little hack to solve this problem.
So I added this code in the action controller:
$form->bind($client);
/** #var $request Request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
/** #var $client Client */
$client = $form->getData();
// hack because of - https://github.com/zendframework/zf2/issues/4694
if($request->getPost('reportSettings') === null){
$client->setReportSettings(null); // set null to remove all associations with this client
}
And also as it is described in the first answere, in form should be rewritten getInputFilterSpecification method, for field that shouldn't be required.

How to create Unique constraint object for form validation?

I need to add the constraints on the fly in the controller action, so I'm doing this:
use Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique;
// ...
$form = $this->createFormBuilder($user)
->add('email', 'email', array(
'constraints' => array(
new NotBlank(),
new MinLength(8),
new MaxLength(100),
new Email(),
new Unique(),
),
))
->getForm();
But I get this error:
The options "fields" must be set for constraint Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique
I tried passing an array('fields' => 'email') and array('fields' => array('email')) to the constructor but didn't work: Warning: get_class() expects parameter 1 to be object, string given in /home/www/dev/public/pixfeed/vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php line 63
So how do I use this class?
The class Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique is usually used on the whole Document. E.g.
/**
* #MongoDB\Document(collection="users")
* #MongoDBUnique(fields="email")
*/
class User
Which in my eyes makes a lot more sense on ORM/ODM level, than just check if the users imput is unique.
But if you are force to do it in Form, you could write a custom constraint.

Symfony2 Doctrine2 Many To Many Form not Saving Entities

I am having some trouble with a many to many relationship. I have Users and Assets. I would like to be able to assign users to an asset on the asset page.
The code below displays a list of users when creating/editing an asset, however changes made to the user checkboxes do not save, while the rest of the data is persisted.
If I add an entry to users_assets through the mysql client, these changes are shown in the asset list.
User
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Asset", inversedBy="users")
*/
private $assets;
}
Asset
class Asset
{
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="assets")
*/
private $users;
}
AssetType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$form = $builder
->add('users', null, array(
'expanded' => true,
'multiple' => true
))
->getForm();
return $form;
}
For some reason I had to switch the doctrine mappings to get this to work:
Asset:
/**
* #ORM\ManyToMany(targetEntity="Adaptive\UserBundle\Entity\User", inversedBy="assets")
* #ORM\JoinTable(name="user_assets")
*/
private $users;
User:
/**
* #ORM\ManyToMany(targetEntity="Splash\SiteBundle\Entity\Asset", mappedBy="users")
*/
private $assets;
Now when I save the asset it saves the users associated. I did not need to define builder->add as an entity or collection. I simply pass it null and it uses the mapping info to fill in the entity info:
AssetType:
->add('users', null, array('expanded' => "true", "multiple" => "true"))
Not exactly sure why I needed to have the inversedBy and JoinTable info on the Asset vs The User but it seems to be working now!
Thanks For The Suggestions!!!
Weird enough I faced the same problem in 2016 and still had hard time finding the solution. I will share it for future googlers:
The problem is that what symfony essentially does when you save the form is this:
$asset->getUsers()->add($user)
And because you're on the inverse side of the relation it won't persist your changes.
What you really need is to make so that it calls this:
$asset->addUser($user)
Where addUser() is defined the following way on the Asset entity:
public function addUser(User $user)
{
//add to the inverse side
$this->users->add($user);
//add on the owning side (only this is persisted)
$user->addAsset($this); //$user->assets->add($asset);
}
So in order to make symfony use that $asset->addUser() method, you should set
'by_reference' => false
on your users field for AssetType form.
More about this setting here http://symfony.com/doc/current/reference/forms/types/form.html#by-reference
Remember you also need to define removeUser() method in the same way (so that it removes entity from the owning relation)
Not exactly sure why I needed to have the inversedBy and
JoinTable info on the Asset vs The User but it
seems to be working now!
The reason why your changes has been ignored is that doctrine persists only changes by the owning side of a relation (like #Florian said).
This is the link to Doctrine's documentation where this behaviour is explained: http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html
At first you should drop backslash prefix in annotations (see notice here).
And you need to use entity field type:
$builder->add('users', 'entity', array(
'class' => 'AdaptiveUserBundle:User',
'expanded' => true,
'multiple' => true,
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
));
You need to use 'collection' field type in your form.
$builder->add('users', 'collection', array(
'type' => new UserType(),
'prototype' => true,
'allow_add' => true,
'allow_delete' => true
));
You need to create the UserType() form first obviously.
Here is all the info you will need, including code samples:
http://symfony.com/doc/current/cookbook/form/form_collections.html
http://symfony.com/doc/current/reference/forms/types/collection.html

Categories