Symfony2/Doctrine UniqueEntity on ManyToOne entity ignored - symfony

I have "roles" associated to "projects".
I don't care if a role name is duplicated, but what I want to make sure is that for each project the role name cannot be duplicated.
Here is what I thought should work:
<?php
// src/AppBundle/Entity/Role.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\RoleRepository")
* #ORM\Table(name="roles")
* #UniqueEntity(fields={"name","project"}, message="Duplicated role for this project")
*/
class Role
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $name;
/**
* #ORM\Column(type="text")
*/
protected $description;
...
other fields
...
/**
* #ORM\ManyToOne(targetEntity="Project")
*/
protected $project;
}
According to the documentation here that's exactly what I need:
This required option is the field (or list of fields) on which this
entity should be unique. For example, if you specified both the email
and name field in a single UniqueEntity constraint, then it would
enforce that the combination value where unique (e.g. two users could
have the same email, as long as they don't have the same name also).
The constraint is simply ignored (I mean that if I try to have the same role name for the same project, it stores the duplicated role name and projectID).
What am I missing?
EDIT: after I updated the DB with "php app/console doctrine:schema:update --force" I tried to generate the error directly with SQL, but no exceptions were thrown. Now, I don't know if this "UniqueEntity" validation is done at DB level or it's Doctrine's validator.
EDIT2: I tried to have only one field ("name") and the validation works properly (only on that field of course). I also tried to have validation on the fields "name" and "description" and it works!!! So basically it does not validate if the field to be validated is the ID pointing to another table.
Anyways, here is the controller:
/**
* #Route("/role/create/{projectID}", name="role_create")
*/
public function createRoleAction(Request $request, $projectID)
{
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($projectID);
$role = new Role();
$form = $this->createForm(new RoleFormType(), $role);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$role->setProject($prj);
$em->persist($role);
$em->flush();
return $this->redirect($this->generateUrl('hr_manage', array('projectID' => $projectID)));
}
return $this->render('Role/createForm.html.twig', array('projectID' => $projectID, 'form' => $form->createView(),));
}
The validation is not performed, and the entity persisted on DB, with the "project" column pointing at the right project. Here is a snapshot of the 2 relevant fields:
Here is the RoleFormType (an extract of relevant fields):
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RoleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// add your custom field
$builder->add('name', 'text')
->add('description', 'text')
...lots of other fields, but "project" is not present as it's passed automatically from the controller
->add('save', 'submit', array('label' => 'Create'));
}
public function getName()
{
return 'role';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Role',));
}
}

The problem is you are not actually validating the entity to check for the unique constraint violation. When you call $form->isValid(), it does call the validator for the Role entity since you passed that as the form's data class. However, since project is not set until after that, no validation occurs on the project field.
When you call $em->persist($role); and $em->flush(); this simply tells Doctrine to insert the entity into the database. Those 2 calls do not perform validation on their own, so the duplicates will be inserted.
Try setting the project before creating the form instead:
$role = new Role();
$role->setProject($prj);
$form = $this->createForm(new RoleFormType(), $role);
Now project will be set on the entity, so when $form->isValid() is called the Symfony validator will check for uniqueness.
If that doesn't work, you'll want to add a project type to the form as a hidden field as well so it's passed back, but I don't think that will be necessary.
The other thing I would state is that you definitely want to add the unique constraint on your database itself - this way even if you try to insert a duplicate, the database will thrown an exception back to you and not allow it, regardless of your code.

Related

Easy Admin 3 (Symfony 4) AssociationField in OneToOne relationship shows already associated entities

Using Symfony 4.4 with Easy Admin 3:
I've a OneToOne relationship
class Usuario
{
...
/**
* #ORM\OneToOne(targetEntity=Hora::class, inversedBy="usuario", cascade={"persist", "remove"})
*/
private $hora;
...
}
class Hora
{
...
/**
* #ORM\OneToOne(targetEntity=Usuario::class, mappedBy="hora", cascade={"persist", "remove"})
*/
private $usuario;
...
}
I've got a CRUD Controller for Usuario:
class UsuarioCrudController extends AbstractCrudController
{
public function configureFields(string $pageName): iterable
{
...
return [
...
AssociationField::new('hora', 'Hora'),
];
Everything seems ok, but in the admin form for "Usuario", the field "hora" shows all values in database, even the ones already assigned to other "Usuario" entities:
I would like the dropdown control to show only not assigned values, PLUS the value of the actual "Usuario" entity, so the control be easy to use.
Which is the proper way to do this with easyadmin?
I've managed to code the field to show only the not associated "Hora" values, using $this->getDoctrine() and ->setFormTypeOptions([ "choices" => in UsuarioCrudController class,
but I am not able to access the actual entity being managed, nor in UsuarioCrudController class (maybe there it is not accesible) neither in Usuario class (I've tried here __construct(EntityManagerInterface $entityManager) to no avail as the value doesn't seem to be injected, dunno why).
It is possible to customize a few things in easy admin by either overriding EasyAdmin methods or listening to EasyAdmin events.
Example of methods:
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
public function createEntity(string $entityFqcn)
public function createEditForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
//etc..
Example of events:
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
You could override easy admin createEditFormBuilder or createNewFormBuilder method, this way you could access the current form data and modify your hora field.
Something like :
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface {
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$unassignedValues = $this->yourRepo->findUnassignedValues();
$data = $context->getEntity()->getInstance();
if(isset($data) && $data->getHora()){
//if your repo return an ArrayCollection
$unassignedValues = $unassignedValues->add($data->getHora());
}
// if 'class' => 'App\Entity\Hora' is not passed as option, an error is raised (see //github.com/EasyCorp/EasyAdminBundle/issues/3095):
// An error has occurred resolving the options of the form "Symfony\Bridge\Doctrine\Form\Type\EntityType": The required option "class" is missing.
$formBuilder->add('hora', EntityType::class, ['class' => 'App\Entity\Hora', 'choices' => $unassignedValues]);
return $formBuilder;
}
Currently, easyadmin3 still lack documentation so sometimes the best way to do something is to look at how easy admin is doing things.
fwiw, the actual entity being edited can be accessed in a Symfony easyadmin CrudController's configureFields() method using:
if ( $pageName === 'edit' ) {
...
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()
...
This way in configureFields() I could add code to filter my entities:
$horas_libres = $this->getDoctrine()->getRepository(Hora::class)->findFree();
and then add the actual entity value also, which is what I was trying to do:
array_unshift( $horas_libres,
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()->getHora() );
Now the field can be constructed in the returned array with "choices":
return [ ...
AssociationField::new('hora', 'Hora')->setFormTypeOptions([
"choices" => $horas_libres
]),
]

Symfony 4 forms, custom DTO and entity relationships

An API has been created to allow Posts to be created with a description and any number of attached Photos.
The problem was that when an API request to edit a Post was received with a null description the typehint would fail.
class Post {
/**
* #Assert\NotBlank
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Photo")
*/
private $photos;
public function setDescription(string $descripton)
Which means instead of the Symfony validation failing Assert\NotBlank it was returning a 500.
This could have been fixed by allowing nulls in the method ?string, this would allow the validation to be called, but result in a dirty entity.
The DTO (Data Transfer Object) approach, a new class to represent the data was created and the validation rules applied to this, this was then added to the form.
class PostData {
/**
* #Assert\NotBlank
*/
public $description;
/**
* #Assert\Valid
* #var Photo[]
*/
public $photos;
The form has was modified:
class PostType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
$builder
->add('description')
->add('photos', EntityType::class, [
'class' => Photo::class,
'multiple' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => PostData::class,
));
}
This worked for description it could be validated without changing the Post entity. If a null was received PostData would trigger the Assert\NotBlank and Post::setDescription would not be called with null.
The problem came when trying to validate Photos existed, if the photo existed it would work, if it didn't there would be a 500 error.
Potentially meaningless 500 error which doesn't indicate the reason
Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is not supported. (500 Internal Server Error)
How can I use DTO PostData to validate a Photo entity exists?
Update composer.json and run composer update
"symfony/http-foundation": "4.4.*",
The issue is related to https://github.com/symfony/symfony/issues/27339
This will give a more meaningful Symfony Form error
Unable to reverse value for property path \"photos\": Could not find all matching choices for the given values
It will also return a lot of extra information if you serilize form errors including DATABASE_URL and APP_SECRET.
I do not recommend running this in production.

How to persist entities from an embedded form submission?

I have a Person entity and an Address entity, set up with a bi-directional one-to-one relationship, with the FK on Address:
...
class Person
{
...
/**
* #ORM\OneToOne(targetEntity="Address", mappedBy="person")
*/
protected $address;
...
}
...
class Address
{
...
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Person", inversedBy="address")
* #ORM\JoinColumn(name="personID", referencedColumnName="id")
*/
protected $person;
}
The Address Entity does NOT have a dedicated primary key, it instead derives its identity through the foreign key relationship with Person, as explained here
The form I have for creating a new Person also embeds the form for Address. When the form is submitted, this controller action is executed:
public function createAction(Request $request)
{
$person = new Person();
$form = $this->createCreateForm($person);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
return $this->redirect($this->generateUrl('people'));
}
return array(
'person' => $person,
'form' => $form->createView(),
);
}
I've inspected the data, and $person has its $address property filled out with the proper form details as expected. However, once the $person object is persisted, I get the error:
A new entity was found through the relationship
'Acme\AcmeBundle\Entity\Person#address' that was not configured to
cascade persist operations for entity...
I've tried a couple of things and none seem to work:
Setting cascade={"persist"} on the OneToOne relationship definition on the Person object. Doing so results in error:
Entity of type Acme\AcmeBundle\Entity\Address is missing an assigned
ID for field 'person'...
On the Person#setAddress method, I've taken the $address parameter and manually called $address->setPerson($this) on it. Doesn't work either.
It seems like my problem is that Doctrine is trying to save the Address object before saving the Person object, and it can't because it needs to know the ID of the associated Person first.
For instance, If I alter the the persist code to something like this, it works:
...
// Pull out the address data and remove it from the Person object
$address = $person->getAddress();
$person->setAddress(null);
// Save the person object and flush so we get an ID
$em->persist($person);
$em->flush();
// Now set the person object on the address and save the address
$address->setPerson($person);
$em->persist($address);
$em->flush();
...
How can I do this properly? I want to retain the ability to embed forms with this type of one-to-one relationship, but things are starting to get complicated. How do I get Doctrine to flush the $person object before flushing the $address object, without manually doing it myself like above?
Keep the cascade=persist.
Then modify Person::setAddress
class Person
{
public function setAddress($address)
{
$this->address = $address;
$address->setPerson($this); //*** This is what you are missing ***
This is a very common question but it's hard to search for.
Your mapping is incorrect. You are using #ORM\Id incorrectly on $person.
If you haven't yet, add a real $id to Address and add again `cascade={"persist"}.
class Person
{
...
/**
* #ORM\OneToOne(targetEntity="Address", mappedBy="person", cascade={"persist"})
*/
protected $address;
...
}
...
class Address
{
...
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="Person", inversedBy="address")
* #ORM\JoinColumn(name="personID", referencedColumnName="id")
*/
protected $person;
}
If Person were the owning side, Address should also be automatically persisted by Doctrine, don't know your model but maybe you should consider changing it.

Get many-to-many entities in a form properly

I'm new with symfony, I looked around but I didn't find the right answer to my problem.
I have two entities linked with a many-to-many relation. Entity User -> Entity FollowedUser.
One User should be able to follow several FollowedUser and one FollowedUser should has several Users who follow him.
My problem is that when I try to list all FollowedUser for one User, say my CurrentUser, I get all FollowedUser not only those associated to my CurrentUser.
Here is my code.
User Entity (src/MyBundle/Entity/User.php) :
namespace MyBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="My_user")
*/
class User extends BaseUser
// ...
/**
* #var FollowedUser[] $followedUsers
*
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\FollowedUser")
*/
private $followedUsers;
// ...
public function getFollowedUsers()
{
return $this->followedUsers;
}
}
UserType:
namespace MyBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use MyBundle\Entity\FollowedUserRepository;
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('followedUsers'); // This shows me the whole table
//,'entity' , array('class' => 'MyBundle\Entity\FollowedUser',
// 'multiple' => true,
// 'query_builder' => function(FollowedUserRepository $followedUserRepo) use ($options) {
// $followedUsers = $options['data']->getFollowedUsers();
// $choices = array();
// foreach ( $followedUsers as $followedUser){
// $choices[] = $followedUser->getId();
// }
// $qb = $followedUserRepo->createQueryBuilder('u');
// $qb->select('u')
// ->where( $qb->expr()->in('u.id',$choices));
// return $qb;
// }
// ));
}
public function getName()
{
return 'followedUser';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\User',
);
}
}
NB: The lines I commented is the only way I found to do what I want. But it does not feel the right way to do it.
In my Controller:
$currentUser = $this->container->get('security.context')->getToken()->getUser();
$followedUsers = $currentUser->getFollowedUsers(); // That works properly
$form = $this->createForm(new UserType(),$currentUser);
EDIT :
Actually my problem was that I forgot some annotation in my ManyToMany declaration. Here is the default annotation which should be used for an unidirectionnal ManyToMany relation:
/**
* #ManyToMany(targetEntity="Group")
* #JoinTable(name="users_groups",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
Solution was found in the doctrine documentation here : doctrine2 unidirectionnal ManyToMany.
If specified, this is used to query the subset of options (and their
order) that should be used for the field. The value of this option can
either be a QueryBuilder object or a Closure. If using a Closure, it
should take a single argument, which is the EntityRepository of the
entity.
Without specifying query_builder Symfony 2 option you'll get all FollowedUser, as you said. The meaning of:
$builder->add('followedUsers');
Is something like:
Add a field whose property is followedUsers of the User class.
Guess it's type (entity type).
query_builder option not specified? Then fetch all users.
Select (depending of expanded and multiple options) those (options) users actually following the user from the model, leaving all other (options) users unselected.
So, question for you is: why you want to display only the users following the user in the form model? It's a no sense... the actual user of the application will never be able to add new following users.

Validating a password in Symfony2

I'm trying to put together a change password feature in Symfony2. I have a "current password" field, a "new password" field and a "confirm new password" field, and the part I'm currently focusing on is validating the "current password" field.
(By the way, I realize now that things like FOSUserBundle exist that would take care of a lot of these things for me, but I already built my authentication system based on the official Symfony documentation, and I don't have time right now to redo all my authentication code.)
What I'm imagining/hoping I can do is create a validation callback that says something like this:
// Entity/User.php
public function currentPasswordIsValid(ExecutionContext $context)
{
$currentPassword = $whatever; // whatever the user submitted as their current password
$factory = $this->get('security.encoder_factory'); // Getting the factory this way doesn't work in this context.
$encoder = $factory->getEncoder($this);
$encryptedCurrentPassword = $encoder->encodePassword($this->getPassword(), $this->getSalt());
if ($encyptedCurrentPassword != $this->getPassword() {
$context->addViolation('Current password is not valid', array(), null);
}
}
As you can see in my comments, there are at least a couple reasons why the above code doesn't work. I would just post specific questions about those particular issues, but maybe I'm barking up the wrong tree altogether. That's why I'm asking the overall question.
So, how can I validate a user's password?
There's a built-in constraint for that since Symfony 2.1.
First, you should create a custom validation constraint. You can register the validator as a service and inject whatever you need in it.
Second, since you probably don't want to add a field for the current password to the User class just to stick the constraint to it, you could use what is called a form model. Essentially, you create a class in the Form\Model namespace that holds the current password field and a reference to the user object. You can stick your custom constraint to that password field then. Then you create your password change form type against this form model.
Here's an example of a constraint from one of my projects:
<?php
namespace Vendor\Bundle\AppBundle\Validator\Constraints\User;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class CurrentPassword extends Constraint
{
public $message = "Your current password is not valid";
/**
* #return string
*/
public function validatedBy()
{
return 'user.validator.current_password';
}
}
And its validator:
<?php
namespace Vendor\Bundle\AppBundle\Validator\Constraints\User;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use JMS\DiExtraBundle\Annotation\Validator;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Inject;
/**
* #Validator("user.validator.current_password")
*/
class CurrentPasswordValidator extends ConstraintValidator
{
/**
* #var EncoderFactoryInterface
*/
private $encoderFactory;
/**
* #var SecurityContextInterface
*/
private $securityContext;
/**
* #InjectParams({
* "encoderFactory" = #Inject("security.encoder_factory"),
* "securityContext" = #Inject("security.context")
* })
*
* #param EncoderFactoryInterface $encoderFactory
* #param SecurityContextInterface $securityContext
*/
public function __construct(EncoderFactoryInterface $encoderFactory,
SecurityContextInterface $securityContext)
{
$this->encoderFactory = $encoderFactory;
$this->securityContext = $securityContext;
}
/**
* #param string $currentPassword
* #param Constraint $constraint
* #return boolean
*/
public function isValid($currentPassword, Constraint $constraint)
{
$currentUser = $this->securityContext->getToken()->getUser();
$encoder = $this->encoderFactory->getEncoder($currentUser);
$isValid = $encoder->isPasswordValid(
$currentUser->getPassword(), $currentPassword, null
);
if (!$isValid) {
$this->setMessage($constraint->message);
return false;
}
return true;
}
}
I use my Blofwish password encoder bundle, so I don't pass salt as the third argument to the $encoder->isPasswordValid() method, but I think you'll be able to adapt this example to your needs yourself.
Also, I'm using JMSDiExtraBundle to simplify development, but you can of course use the classical service container configuration way.
In Symfony 2.1 you can use the built-in validator:
http://symfony.com/doc/master/reference/constraints/UserPassword.html
So for instance in your form builder:
// declare
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
// mapped=>false (new in 2.1) is to let the builder know this is not an entity field
->add('currentpassword', 'password', array('label'=>'Current password', 'mapped' => false, 'constraints' => new UserPassword()))
Apparently there's a bug right now with that validator so might or might now work
https://github.com/symfony/symfony/issues/5460
FOSUserBundle uses a ModelManager class which is separate from the base Model. You can check their implementation.
I ended up cutting the Gordian knot. I bypassed all of Symfony's form stuff and did all the logic in the controller.

Resources