FOSUserBundle, EventListener registration user - symfony

I'm working on the FOSUserBundle, on EventListener for RegistrationUser.
In this bundle, when I create a user, I use a method updateUser() (in Vendor...Model/UserManagerInterface). This method seems to be subject to an EventListener that triggers at least two actions. Registering the information entered in the database. And sending an email to the user to send them login credentials.
I found the method that sends the mail. By cons, I didn't find the one who makes the recording. I also didn't find where to set the two events.
First for all (and my personal information), I try to find these two points still unknown. If anyone could guide me?
Then, depending on what we decide with our client, I may proceed to a surcharge (which I still don't really know how to do), but I imagine that I would find a little better once my two strangers found
Thanks for your attention and help

This is the function wich handles the email confirmation on registrationSucces
FOS\UserBundle\EventListener\EmailConfirmationListener
public function onRegistrationSuccess(FormEvent $event)
{
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setEnabled(false);
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
$this->mailer->sendConfirmationEmailMessage($user);
$this->session->set('fos_user_send_confirmation_email/email', $user->getEmail());
$url = $this->router->generate('fos_user_registration_check_email');
$event->setResponse(new RedirectResponse($url));
}
But I tell you that what you are trying to do is a bad practice. The recommended way is the following.
Step 1: Select one of the following events to listen(depending on when you want to catch the process)
/**
* The REGISTRATION_SUCCESS event occurs when the registration form is submitted successfully.
*
* This event allows you to set the response instead of using the default one.
*
* #Event("FOS\UserBundle\Event\FormEvent")
*/
const REGISTRATION_SUCCESS = 'fos_user.registration.success';
/**
* The REGISTRATION_COMPLETED event occurs after saving the user in the registration process.
*
* This event allows you to access the response which will be sent.
*
* #Event("FOS\UserBundle\Event\FilterUserResponseEvent")
*/
const REGISTRATION_COMPLETED = 'fos_user.registration.completed';
Step 2 Implement the Event Subscriber with a priority
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => [
'onRegistrationSuccess', 100 //The priority is higher than the FOSuser so it will be called first
],
);
}
Step 3 Implement your function
public function onRegistrationSuccess(FormEvent $event)
{
//do your logic here
$event->stopPropagation();//the Fos User method shall never be called!!
$event->setResponse(new RedirectResponse($url));
}
You never should modify the third party libraries in this case the Event Dispatcher System is made for this to earlier process the event and if its needed stop the propagation and avoid the "re-processing" of the event.
Hope it helps!!!!

Related

Symfony, wrong user logged by reset-password link

On my platform, the administrator create a user where the password is randomly generated and this automatically sends an email to this new user. The email contains a link that leads to the reset-password page (which will be a password creation page for the user because he does not know that he already has a password generated).
The problem is that when the user clicks on the email link and arrives on the change password page, he is logged in as admin and therefore has permissions that he should not have.
In fact, I want the email link to connect the new user to his account, I don't want him to be logged in as admin. I'm not sure how to do this.
I don't know much about tokens. I believe the Token is generated based on the session used (?).
Thank you in advance for your help.
Here is the code for creating a user :
/**
* #Route("/new", name="user_new", methods={"GET", "POST"})
* #throws TransportExceptionInterface
*/
public function new(Request $request, MailSender $mailSender,UserPasswordHasherInterface $passwordHasher): Response
{
// TODO CHECK IF USER ALREADY EXISTS BY EMAIL
$user = new User();
$form = $this
->createForm(UserType::class, $user)
->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// TODO GENERATE RANDOM PASSWORD
//$passwordHasher->hashPassword($user, $user->getPassword()));
$user->setPassword($passwordHasher->hashPassword($user, "password"));
$this->entityManager->persist($user);
$this->entityManager->flush();
try {
$resetToken = $mailSender->makeToken($user);
} catch (ResetPasswordExceptionInterface $e) {
return $this->redirectToRoute('user_new');
}
$mailInfos = array('template'=>"reset_password/email_activate.html.twig", 'subject'=>"Activer votre compte", 'email'=>$user->getEmail());
$mailSender->sendMail($resetToken, $mailInfos);
$mailSender->storeToken($resetToken);
return $this->redirectToRoute('user_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('user/new.html.twig', [
'user' => $user,
'form' => $form,
]);
}
This is expected behaviour because:
multiple tabs/instances of the same browser will usually share the
same server-side session when interacting with the same domain.
means that you can´t be logged in with different users in different tabs per default.
And I don´t think that you would want this, just think of the downsides, do you really want to login again for every tab? This is very uncommon practice. Imagine you would open a stack-overflow question in a new tab and you would not be logged in there.
There are ways to achieve this though, but really re-think if thats your actual usecase, i don´t think so, you are just developing your feature and testing it, and in production a new user will not be already logged in as admin is my assumption.
So for testing your feature just use a private tab (that does usually not share the same server-side session )
if you want to learn more i found this pretty cool so-thread where users try to explain as best as possible
What are sessions? How do they work?

Behat: expect to send an email

Most of the few Behat examples I saw test for an object property, like
/**
* #Then the overall basket price should be £:price
*/
public function theOverallBasketPriceShouldBePs($price)
{
PHPUnit_Framework_Assert::assertSame(
floatval($price),
$this->basket->getTotalPrice()
);
}
But what if my user story goes something like this:
Given, a user has received pdf1
When 48 hours have passed since the download
Then the application must send pdf2 to the user
How am I supposed to test for the #then step in this case - mocking and using PHPUnit expectations? I somehow get the feeling that I completely misunderstand something here.
Since I want my domain object to not have any dependencies, I opted to purely test the domain logic that involves sending messages, nothing more. As far as I understand it, this is what BDD is about. In my Lead entity:
public function isEligibleForNextMessage( int $interval ) {
if ( /* Do Logic */ ) :
return TRUE;
endif;
return FALSE;
}
Then in my LeadContext:
/**
* #Then the application must send pdf2 to the user
*/
public function sendNextMessage() {
Assert::assertTrue( $this->Lead->isEligibleForNextMessage( 172800 ) );
}
This should be be sufficient to test the above user story regarding the time interval.

Tow Registration form FosUserBundle Symfony

I work with symfony 2.8 and FOSUserBundle, I have two type of user in the same table in database , And I like to differenciate the registration form in the same page of registration like this :
*
the problem that I can't use two instance of the form in the same page, what can I do please?
The way I would go about this, is override FOSUserBundle and then extend the RegistrationController and likely the corresponding template.
In the registerAction you can reuse some parts of the original, but where the form is created you then create two different ones, maybe like this:
/** #var $formFactory FactoryInterface */
$clientFormFactory = $this->get('client_registration.form.factory');
$clientForm = $clientFormFactory->createForm();
$clientForm->setData($client);
/** #var $formFactory FactoryInterface */
$correspondentFormFactory = $this->get('correspondent_registration.form.factory');
$correspondentForm = $correspondentFormFactory->createForm();
$correspondentForm->setData($correspondent);
$clientForm->handleRequest($request);
$correspondentForm->handleRequest($request);
if ($clientForm->isSubmitted() && $clientForm->isValid()) {
// ...
} elseif ($correspondentForm->isSubmitted() && $correspondentForm->isValid()) {
// ...
}
return $this->render(
'#FOSUser/Registration/register.html.twig',
[
'clientForm' => $clientForm->createView(),
'correspondentForm' => $correspondentForm->createView(),
]
);
The part inside the if conditions will then probably look similar as to the original controller. You might have different UserManager's for each user type, you have to switch out, but other than that it's basically: dispatch pre-event, save user, dispatch post-event, redirect. It is important that you dispatch both events as other parts of FOSUserBundle will rely on them, e.g. sending a registration email.
In your template you then just render both forms in their tab. You might have to fiddle around with the form id's a bit, but that should be straightforward.

Symfony2 - set an entity so its properties are "read only"

We're using Symfony 2.3 and Doctrine to manage entities.
Suppose we've an entity called "Task" which a user can create as usual using the Symfony form builder and can persist using Doctrine. The "Task" might be an instruction to send some emails on a certain date, for example. Suppose the "Task" has a property called "status". "Tasks" that have been created correctly might have a status of "Ready".
A cron job periodically calls a symfony Custom Command to check if there are any "Tasks" that need processing (i.e. have a status of "Ready") and if it finds any, it performs some action and then updates the entity to have a status of "Completed".
Once a Task has been processed and its status has been set to be "Completed", is there any way of making the entity effectively "read only"? By this, I mean that a user could still see details of the Task, but they would not be able to change of its properties using a form? Bear in mind that the user might browse to the "edit" form for a particular Task, and whilst they are reading the details, the cron job might process the task and update the "status" property of the Task - the user might then submit the form without knowing that the Task has already been processed, and the form handler could attempt to persist the entity, possibly setting the "status" back to be "Ready" - thereby ensuring that the same task gets processed again the next time the cron job runs.
If, however, the Task was made read-only when its status gets set to be "Completed", the form submission would have no effect.
You should do this in your CRUD. Upon calling the "editAction" you must check the status on the entity. If it is "Completed" then you wouldn't display the edit form, but rather redirect the user to the "showAction"
For example:
/**
* Displays a form to edit an existing Task entity.
*
* #Route("/{id}/edit", name="task_edit")
* #Method("GET")
* #Template()
*/
public function editAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if($entity->isCompleted())
return $this->forward('AcmeTaskBundle:Task:show', array('id' => $id ));
//Else finish edit action
}
/**
* Finds and displays a Task entity.
*
* #Route("/{id}", name="task_show")
* #Method("GET")
* #Template()
*/
public function showAction($id) {
...
}
Additionally, you can do the same thing in your update Action once you have checked the validity of the form. Just in case they have the edit form open right before the cron is run.
/**
* Edits an existing Task entity.
*
* #Route("/{id}", name="task_update")
* #Method("PUT")
* #Template("AcmeTaskBundle:Task:edit.html.twig")
*/
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AcmeTaskBundle:Task')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Task entity.');
}
if($entity->isCompleted())
//Redirect Again. Note that this happens BEFORE the flush() which is when the entity is persisted to the db
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->flush();
return $this->redirect($this->generateUrl('administration_product_edit', array('id' => $id)));
}
...
}
Furthermore, if the Task is completed, they you should probably remove the "Edit option" from the showAction user interface all together. That would mean, in your twig template you check if the entity is completed BEFORE rendering the "Edit button"

How to customize symfony2 forms upon retrieval?

I have a case where we create registration for sports events.
The registration contains some fields specific to each sport. Some of which will be named similarly although they will be different for each sport. Example: "favorite position on the field":
For Basketball it would be a choice field between:
Point guard
Shooting guard
etc...
For baseball, it would be the same choice field but with some different choices available:
Pitcher
Infield
Outfield
...
When first creating the form (for display), the sport is passed as part of the data in the registration:
$registration = new Registration;
$registration->setEvent($event);
and $event->getSport(); would return the sport for that event.
So far so good, and adding a listener to the generation of my form, I can set only the fields specific to that sport:
public static function getSubscribedEvents()
{
return [FormEvents::POST_SET_DATA => 'preSetData'];
}
/**
* #param event DataEvent
*/
public function preSetData(DataEvent $event)
{
$form = $event->getForm();
if (null === $event->getData()) {
return;
}
// (The get event here means the real life sports gathering)
$sport = $event->getData()->getEvent()->getSport();
/**
* Then I customize the fields depending on the current sport
*/
}
The problem comes when the user submits this form back. In this case, $event->getData()->getEvent() is null.
The "event" (real life one) is a document_id field in the registration form (using MongoDB here).
If I listen to the ::BIND event instead of ::PRE_SET_DATA, then I can access everything, but it's too late to customize the form as it is already bound. ::PRE_BIND does the same as ::PRE_SET_DATA.
How can I correctly retrieve my Event and Sport Documents here in order to customize my form and validate it appropriately?
Why would you need an event to do such task? You can define the fields in the buildForm() action of the form class. To access the event object simply use $options['data']->getEvent()
So ... Finally found how to do this properly.It requires subscribing to two different events.
First time the form is built, some data is passed to it, therefore, the PRE_SET_DATA event contains that data and everything works fine as explained in the question.
On the moment the form is submitted, it is first created with NO data, therefore the data accessed in PRE_SET_DATA will be null. In this case we skip over the form customization:
public function preSetData(DataEvent $event)
{
$myEvent = $event->getData()->getEvent();
if (null === $myEvent) {
return;
}
$this->customizeForm();
}
This ensures that we don't run into issues when submitting the form and no data is passed, however getData() will return an empty object and not NULL.
Now, when the form is submitted, we will bind it to the data received. That's when we want to interfere. So we'll also subscribe to the PRE_BIND event:
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_BIND => 'preBind',
FormEvents::PRE_SET_DATA => 'preSetData',
];
}
In pre-bind, the data we receive is only an array of values and not an object graph.
But if we injected the object manager in our listener, then we can find our objects and work with them:
public function preBind(DataEvent $event)
{
$data = $event->getData();
$id = $data['event'];
$myEvent = $this->om
->getRepository('Acme\DemoBundle\Document\Event')
->find(new \MongoId($id));
if($myEvent === null){
$msg = 'The event %s could not be found';
throw new \Exception(sprintf($msg, $id));
}
$this->customizeForm();
}

Resources