Trigger UniqueEntity after persisting and before flushing data - symfony

I'm importing a kind of csv data via doctrine entity manger, however, I have a loop that performs a batch processing as it is mentioned at the level of the doc.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/batch-processing.html#bulk-inserts
$validationErrors = [];
foreach($data as $itrationNumber => $item) {
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
$errors = $this->validator->validate($user);
if ($errors->count() === 0) {
$this->persist($user);
} else {
$validationErrors[] = $errors;
}
if ($itratioNumber% 100 === 0) {
$this->em->flush();
}
}
return $validationErrors;
Here is my User class, which has a unique constraint on the email field:
/**
* #ORM\Entity
* #UniqueEntity("email")
*/
class User
{
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
* #Assert\Email
*/
protected $email;
}
Unfortunately, if there is more than one row in my data that has an identical email address, UniqueEntity validation will not be triggered, simply, because users are persisted but are not flushed into the database.
Solution 1: Avoid the batch and do a flush at each iteration which is very violating and may provoke connection closed doctrine or kind of memory leak.
Solution 2: It was to create a custom constraint, which is inspired by UniqueEntity("email") and then check for each item's email address if there's a user already persisted with the same mail.
The problem that if the user already exists in the database, and we call em-> persist(), I cannot find any persisted object neither in $entityManager->getUnitOfWork()->getScheduledEntityInsertions() nor in $entityManager->getUnitOfWork()->getScheduledEntityUpdates().
Only during a new insertion that I hole the object persisted in the response of the function ->getScheduledEntityInsertions()
I would be very grateful if anyone has any idea how I could recover the entities persisted after the $entityManager->persist() step.
Or simply a 3rd solution which allows me to trigger a validation on the uniqueness of the email even in a batch context.

You should keep a second bit of information to see if an e-mail address was already processed:
$emails = [];
foreach($data as $itrationNumber => $item) {
if (isset($emails[$item['email']])) {
continue;
}
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
if ($this->validator->validate($user)->count() === 0) {
$this->persist($user);
$emails[$item['email']] = true;
}
if ($itratioNumber% 100 === 0) {
$this->em->flush();
}
}
This code snippet does not normalize the e-mail address, this should be done first (strtolower and maybe removal of +foo gmail local parts for example).

Except if yo are WILLING to load the whole emails in a memory cache and play around that with REDIS or direct arrays, I can't see a second way but using the DB constraint of uniqueness (i.e doctrine flush)
Regarding your fear of memory overload, you can simply detach the persisted entity from the managed pool by calling the clear method. So your code would look something like this:
foreach($data as $itrationNumber => $item) {
/** #var User|null $user **/
$user = $this->em->getRepository(User::class)->findOnBy(['email' => $item['email']]);
$user = ($user) ? $user : new User();
$user->setName('test');
if ($this->validator->validate($user)->count() === 0) {
$this->persist($user);
$this->em->flush();
}
$this->em->clear();
}
I personnaly used this solution for a batch processing of huge CSV flow, and got inspired from this doctrine documentation section.
P.S I would think about DB transactions but might need more tricks.

Related

Symfony 4 - Creation form - Object is null

Symfony 4.3
Custom users provider (no FOS)
PHP 7.1 / MariaDB 10.2
Wamp local server
I already made users edit and users delete functions. It work PERFECT !
Now I want to create a registration form in my website but I don't understand the error !
Return value of App\Entity\User::getFirstname() must be of the type string, null returned
The exception :
in \src/Entity/User.php (line 100)
/**
* #ORM\Column(type="string")
* #Assert\NotBlank()
*/
private $firstname;
public function getFirstname(): string
{
return $this->firstname; // THIS IS LINE 100
}
Below is an extract from my UserController :
/**
* #Route("/users/add", name="app_users_add")
*/
public function addUser(Request $request, EntityManagerInterface $em): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user); // This line generates the error
[...]
return $this->render('user/add.html.twig', [
'form' => $form->createView()
]);
}
I really don't understand.
My UserEntity is working with my edit method in controller.
MY template user/add.html.twig is good (even if I let it empty)
My UserType form builder work well (I use the same for editing users)
you should add
public function getFirstname(): ?string
return $this->firstname;
}
The reason $form = $this->createForm(UserType::class, $user); generates an error is because you are passing $user as data to the form UserType. (this is refering to a file App\Form\UserType, right?). Since $user is a new object, $firstname of this user is not yet defined, thus the error occurs when you are passing the data of $user to the form. Simply removing the $user argument will work.
If you do not have a file App\Form\UserType, you can either create it or use createFormBuilder() in your controller. See the documentation for more information.

Symfony - Edit User Entity without changing password

I'm running Symfony 3.4.14 and I developed my own User Bundle, I have a very bad experience with FOS then I don't want to use anymore. My goal is to let the admin users create/edit/remove users, I mean other users account.
I made :
the User entity
the Login form
the Registration form
.. and I'm stuck with the Update form. I want to let the admins edit a user without editing the password, but to give them the opportunity to do it if needed. Below is my EditUserAction in controller :
<?php
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function EditUserAction($id, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $this->getDoctrine()->getRepository('CoreBundle:User')->findOneBy([ 'id'=>$id, 'deleted' => 0 ]);
if ( $user )
{
$old_password = $user->getPassword();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// If admin changed the user password
if ( $user->getPlainPassword() )
{
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
}
// If admin didn't change the user password, we persist the old one
else
{
$user->setPassword($old_password);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
}
return array('form' => $form->createView());
}
return $this->redirectToRoute('admin_users');
}
Case 1 (when admin choose to change the user password) works well, but the other case (when admin don't want to change the password) fails. They are no way to let the 2 password inputs empty. I can't get rid of this validation error in the debug toolbar :
Path: data.plainPassword
Message: This value should not be blank
In order to avoid this error, as you can see in my controller above, I try to keep the old one (may not be a best practice, I know).
In your form / entity (where you defined validations) you should write either your own constraint (documentation) or validation callback (documentation). In there you can check - If value is null, don't validate, if not null run your validations.
You need to have two separate methods for both.
public function changePasswordAction(Request $request)
{
// your code for changing password.
}
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editUserAction(User $user = null, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
//you can directly give your User Entity reference in parameters and you dont need to write an extra query to find user.
if ( $user === null){
//return user not found
}
else if($user->isDeleted() === false)
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
//your code
}
// your code
}
return $this->redirectToRoute('admin_users');
}

Setting a default value for a one-on-one association

Doctrine is a beast in combination with Symfony, but I can't seem to find good examples on how to achieve what I need.
My user schema is pretty standard. I want to set a one-to-one association that points to the forum permission a user has.
For this to work, I need to create a "default entity" that holds the default permissions given to the user upon creation.
Here is the User#forumPermission association
/**
* #var $forumPermissions ?ForumPermission
* One User instance has One Forum Permission instance.
* #ORM\OneToOne(targetEntity="ForumPermission")
* #JoinColumn(name="forum_permission_id", referencedColumnName="id")
*/
private $forumPermission;
/**
* #return ForumPermission
*/
public function getForumPermission() : ?ForumPermission
{
return $this->forumPermission;
}
/**
* #param ForumPermission $forumPermission
*/
public function setForumPermission(ForumPermission $forumPermission): void
{
$this->forumPermission = $forumPermission;
}
This means the forum_permission table is empty at this time.
After that, I read in the doctrine docs that you can listen to all kinds of events related to flushing and persisting. Here is the docs sections for onFlush
This is what I came up with
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
/* #var $entity User */
if (true === $entity instanceof User
&& null === $entity->getForumPermission()) {
$entity->setForumPermission($this->getDefaultForumPermission($em, $uow));
}
}
}
private function getDefaultForumPermission(
\Doctrine\ORM\EntityManagerInterface $em,
\Doctrine\ORM\UnitOfWork $uow)
{
// Get the default permissions form the database
$defaultForumPermission = $em->getRepository(ForumPermission::class)->find(1);
// I will create a new entity if the default permissions do not exist
if (null === $defaultForumPermission) {
$defaultForumPermission = new ForumPermission();
$uow->persist($defaultForumPermission);
$uow->computeChangeSet($em->getClassMetadata('\App\Entity\ForumPermission'), $defaultForumPermission);
}
return $defaultForumPermission;
}
The docs aren't very clear on what you can do and where you should do it.
I figure I could do this all in the controller, but I like to keep everything where it belongs as intended by design.
So I'm wondering if this is the best way to do it, should I be doing this during prePersist or another event? Any help is greatly appreciated.
If I get it right, each time a User is created you want to create a new ForumPermission associated to this User. The best way to do it is to listen to the prePersist Doctrine event.
See https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html for additional information.

am new with symfony and i need some advices

I have a problem regarding a Symfony application, I want to take as input the "username" or "id" for my controller , and receive information that is in my table "user" and also 2 other table for example : A user has one or more levels , and also it has points must earn points to unlock a level , I want my dan Home page display the username and the level and extent that it has , I jn am beginner and not come to understand the books symfony that I use, I work with PARALLEL " symfony_book " and " symfony_cook_book " and also tutorial youtube May I blocks , here is the code for my cotroler
"
/**
* #Route("/{id}")
* #Template()
* #param $id=0
* #return array
*/
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
//return ['id' => $id,'username' => $username];
return array('username' => $username);
}
and I have to use the relationship among classes
use Doctrine\Common\Collections\ArrayCollection;
class Experience {
/**
* #ORM\OneToMany(targetEntity="FosUser", mappedBy="experience")
*/
protected $fosUsers;
public function __construct()
{
$this->fosUsers = new ArrayCollection();
}
}
and
class FosUser {
/**
* #ORM\ManyToOne(targetEntity="Experience", inversedBy="fosUsers")
* #ORM\JoinColumn(name="experience_id", referencedColumnName="id")
*/
protected $fosUsers;
}
and i have always an error
In Symfony you cannot return an array in Action function!, Action function must always return a Response object...So if you want to return data to browser in Symfony, Action function have to return a string wrapped up in Response object.
In your controller code, to return the array to browser, You can serialize an array to JSON and send it back to browser:
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
return new Response(json_encode(array('username' => $username)));
}
I suggest you to read more about HTTP protocol , PHP, and Symfony.

How to disable Blameable-behaviour programmatically in Symfony2

I'm trying to run a console command in symfony2 in which some properties of a certain class are being updated. One of the properties has got a corresponding reviewedBy-property which is being set by the blameable-behaviour like so:
/**
* #var bool
* #ORM\Column(name="public_cmt", type="boolean", nullable=true)
*/
private $publicCmt;
/**
* #var User $publicCmtReviewedBy
*
* #Gedmo\Blameable(on="change", field="public_cmt")
* #ORM\ManyToOne(targetEntity="My\Bundle\EntityBundle\Entity\User")
* #ORM\JoinColumn(name="public_cmt_reviewed_by", referencedColumnName="id", nullable=true)
*/
private $publicCmtReviewedBy;
When i run the task there's no user which can be 'blamed' so I get the following exception:
[Doctrine\ORM\ORMInvalidArgumentException]
EntityManager#persist() expects parameter 1 to be an entity object, NULL given.
However I can also not disable blameable because it's not registered as a filter by the time i start the task and programmatically trying to set the user through:
// create the authentication token
$token = new UsernamePasswordToken(
$user,
null,
'main',
$user->getRoles());
// give it to the security context
$this->getService('security.context')->setToken($token);
doesn't work. Anyone got an idea?
If you use the StofDoctrineExtensionsBundle you can simply do :
$this->container->get('stof_doctrine_extensions.listener.blameable')
->setUserValue('task-user');
see : https://github.com/stof/StofDoctrineExtensionsBundle/issues/197
First of all, I'm not sure if 'field' cares if you use the database column or the property, but you might need to change it to field="publicCmt".
What you should do is override the Blameable Listener. I'm going to assume you are using the StofDoctrineExtensionsBundle. First override in your config:
# app/config/config.yml
stof_doctrine_extensions:
class:
blameable: MyBundle\BlameableListener
Now just extend the existing listener. You have a couple options - either you want to allow for NULL values (no blame), or, you want to have a default user. Say for example you want to just skip the persist and allow a null, you would override as such:
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
public function getUserValue($meta, $field)
{
try {
$user = parent::getUserValue($meta, $field);
}
catch (\Exception $e) {
$user = null;
return $user;
}
protected function updateField($object, $ea, $meta, $field)
{
if (!$user) {
return;
}
parent::updateField($object, $ea, $meta, $field);
}
}
So it tries to use the parent getUserValue() function first to grab the user, and if not it returns null. We must put in a try/catch because it throws an Exception if there is no current user. Now in our updateField() function, we simply don't do anything if there is no user.
Disclaimer - there may be parts of that updateField() function that you still need...I haven't tested this.
This is just an example. Another idea would be to have a default database user. You could put that in your config file with a particular username. Then instead of returning null if there is no user from the security token, you could instead grab the default user from the database and use that (naturally you'd have to inject the entity manager in the service as well).
Slight modification of the above answer with identical config.yml-entry: we can check if a user is set and if not: since we have access to the object-manager in the updateField-method, get a default-user, set it and then execute the parent-method.
namespace MyBundle\EventListener;
use Gedmo\Blameable\BlameableListener;
class MyBlameableListener extends BlameableListener
{
protected function updateField($object, $ea, $meta, $field)
{
// If we don't have a user, we are in a task and set a default-user
if (null === $this->getUserValue($meta, $field)) {
/* #var $ur UserRepository */
$ur = $ea->getObjectManager()->getRepository('MyBundle:User');
$taskUser = $ur->findOneBy(array('name' => 'task-user'));
$this->setUserValue($taskUser);
}
parent::updateField($object, $ea, $meta, $field);
}
}

Resources