I've an entity with a plainPassword and a password attribute. In form, I map on the plainPassword. After, when the user valid the form, I do password validation on the plainPassword.
To encode the password, I use an EventSubscriber that listen on prePersist and preUpdate. It works well for the register form, because it's a new entity, the user fill some persisted attributes, then doctrine persist it and flush.
But, when I just want to edit the password, it doesn't work, I think it's because the user just edit a non persisted attribute. Then Doctrine doesn't try to persist it. But I need it, to enter in the Subscriber.
Someone know how to do it ? (I've a similar problem in an other entity) For the moment, I do the operation in the controller...
Thanks a lot.
My UserSubscriber
class UserSubscriber implements EventSubscriber
{
private $passwordEncoder;
private $tokenGenerator;
public function __construct(UserPasswordEncoder $passwordEncoder, TokenGenerator $tokenGenerator)
{
$this->passwordEncoder = $passwordEncoder;
$this->tokenGenerator = $tokenGenerator;
}
public function getSubscribedEvents()
{
return array(
'prePersist',
'preUpdate',
);
}
public function prePersist(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->createActivationToken($object);
$this->encodePassword($object);
}
}
public function preUpdate(LifecycleEventArgs $args)
{
$object = $args->getObject();
if ($object instanceof User) {
$this->encodePassword($object);
}
}
private function createActivationToken(User $user)
{
// If it's not a new object, return
if (null !== $user->getId()) {
return;
}
$token = $this->tokenGenerator->generateToken();
$user->setConfirmationToken($token);
}
private function encodePassword(User $user)
{
if (null === $user->getPlainPassword()) {
return;
}
$encodedPassword = $this->passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($encodedPassword);
}
My user Entity:
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
private $email;
/**
* #Assert\Length(max=4096)
*/
private $plainPassword;
/**
* #ORM\Column(name="password", type="string", length=64)
*/
private $password;
ProfileController:
class ProfileController extends Controller
{
/**
* #Route("/my-profile/password/edit", name="user_password_edit")
* #Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
public function editPasswordAction(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(ChangePasswordType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Encode the password
// If I decomment it, it's work, but I want to do it autmaticlally, but in the form I just change the plainPassword, that is not persisted in database
//$password = $this->get('security.password_encoder')->encodePassword($user, $user->getPlainPassword());
//$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success', 'Your password have been successfully changed.');
return $this->redirectToRoute('user_profile');
}
return $this->render('user/password/edit.html.twig', [
'form' => $form->createView(),
]);
}
}
You can force Doctrine to mark an object as dirty by manipulating the UnitOfWork directly.
$em->getUnitOfWork()->scheduleForUpdate($entity)
However, I do strongly discourage this approach as it violates carefully crafted abstraction layers.
Related
In my application, once a user logs in, taken to the home page where he can view his details. There is a button "Edit Profile" where the user will be taken to a page where he can edit the data. Once the editing successes, he is redirected back to the home page. But here, it is redirected to the login page. I think the session is expired unexpectedly. How to overcome this issue?
// This is my update info controller
/**
* #Route("/update/{id}", name="update")
* #param $id
* #param Request $request
* #param UserPasswordEncoderInterface $passwordEncoder
* #param UserInterface $loggedUser
* #return \Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function updateUser($id,Request $request, UserPasswordEncoderInterface $passwordEncoder, UrlGeneratorInterface $urlGenerator){
$loggedUser = $this->get('security.token_storage')->getToken()->getUser()->getId();
if ($id == $loggedUser){
$em = $this->getDoctrine()->getManager();
$conn =$em->getConnection();
$user = $em->find(User::class,$id);
$form = $this->createForm(RegisterType::class,$user, [
'validation_groups' => ['update'],
]);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$file = $request->files->get('register')['image'];
if($file){
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move(
$this->getParameter('uploads_dir'), $fileName
);
$user->setImage($fileName);
}
if($user->getPassword() !="") {
$user->setPassword($passwordEncoder->encodePassword($user,$user->getPassword()));
$sql = '
UPDATE user
SET first_name = :firstName, last_name = :lastName, id_number = :idNumber, phone_number = :phoneNumber, address = :address, password = :password
WHERE id= :id
';
$stmt = $conn->prepare($sql);
$stmt->execute(['firstName' => $user->getFirstName(),
'lastName' => $user->getLastName(),
'idNumber' => $user->getIdNumber(),
'phoneNumber' => $user->getPhoneNumber(),
'address' => $user->getAddress(),
'password' => $user->getPassword(),
'id' => $id]);
} else {
$sql = '
UPDATE user
SET first_name = :firstName, last_name = :lastName, id_number = :idNumber, phone_number = :phoneNumber, address = :address
WHERE id= :id
';
$stmt = $conn->prepare($sql);
$stmt->execute(['firstName' => $user->getFirstName(),
'lastName' => $user->getLastName(),
'idNumber' => $user->getIdNumber(),
'phoneNumber' => $user->getPhoneNumber(),
'address' => $user->getAddress(),
'id' => $id]);
}
return new RedirectResponse($urlGenerator->generate('home'));
}
} else {
return new RedirectResponse($urlGenerator->generate('home'));
}
return $this->render('register/update.html.twig', [
'form'=>$form->createView(),
]);
}
// This is RegisterType form
class RegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email',EmailType::class,[
'label'=>'Email',
'required' => false,
'attr'=>['placeholder'=>"Email"]
])
->add('password',RepeatedType::class,[
'type' => PasswordType::class,
'invalid_message' => 'The password fields must match.',
'required' => false,
'options' => ['attr' => ['class' => 'password-field']],
'first_options' => ['label' => 'Password','attr'=>['placeholder'=>"Password"]],
'second_options' => ['label' => 'Confirm Password','attr'=>['placeholder'=>"Confirm Password"]],
])
->add('firstName',TextType::class,['label'=>'First Name', 'attr'=>['placeholder'=>"First Name"]])
->add('lastName',TextType::class,['label'=>'Last Name','attr'=>['placeholder'=>"Last Name"]])
->add('address',TextareaType::class,['required' => false,'label'=>'Address','attr'=>['placeholder'=>"Address"]])
->add('idNumber',TextType::class,['label'=>'NIC Number','attr'=>['placeholder'=>"NIC Number"]])
->add('phoneNumber',TelType::class,['label'=>'Phone Number','attr'=>['placeholder'=>"Phone Number"]])
->add('image',FileType::class,['label'=>'Photo','required'=>false,'attr'=>['hidden'=>"hidden", 'accept'=>"image/jpeg, image/png"]])
->add('save',SubmitType::class,[
'label'=>'Register',
'attr' => [
'class'=>"btn btn-outline-success float-right"
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
// This is my User Class
class User implements UserInterface{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
* #Assert\Email()
* #Assert\NotBlank()
*/
private $email;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
* #Assert\NotBlank()
*/
private $password;
/**
* #ORM\Column(type="string",length=255)
* #Assert\NotBlank(groups={"update"})
*
*/
private $firstName;
/**
* #ORM\Column(type="string",length=255)
* #Assert\NotBlank(groups={"update"})
*/
private $lastName;
/**
* #ORM\Column(type="string",length=255,nullable=true)
*
*/
private $image;
/**
* #ORM\Column(type="string", nullable=true)
*/
private $address;
/**
* #ORM\Column(type="string",length=10)
* #Assert\Length("10",groups={"update"})
*/
private $phoneNumber;
/**
* #ORM\Column(type="string",length=10)
* #Assert\NotBlank(groups={"update"})
* #Assert\Length("10",groups={"update"})
*/
private $idNumber;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Vehicle", mappedBy="user")
*/
private $vehicle;
/**
* #ORM\OneToOne(targetEntity="App\Entity\Account", inversedBy="user")
*/
private $account;
public function __construct()
{
$this->vehicle = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail()
{
return $this->email;
}
public function setEmail( $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername()
{
return (string) $this->email;
}
public function getRoles(): ?array
{
return $this->roles;
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getFirstName()
{
return $this->firstName;
}
public function setFirstName( $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName()
{
return $this->lastName;
}
public function setLastName( $lastName): self
{
$this->lastName = $lastName;
return $this;
}
public function getImage(): ?string
{
return $this->image;
}
public function setImage(string $image): self
{
$this->image = $image;
return $this;
}
public function getAddress()
{
return $this->address;
}
public function setAddress( $address): self
{
$this->address = $address;
return $this;
}
public function getIdNumber()
{
return $this->idNumber;
}
public function setIdNumber( $idNumber): self
{
$this->idNumber = $idNumber;
return $this;
}
public function getPhoneNumber()
{
return $this->phoneNumber;
}
public function setPhoneNumber( $phoneNumber): self
{
$this->phoneNumber = $phoneNumber;
return $this;
}
/**
* #return Collection|Vehicle[]
*/
public function getVehicle(): Collection
{
return $this->vehicle;
}
public function addVehicle(Vehicle $vehicle): self
{
if (!$this->vehicle->contains($vehicle)) {
$this->vehicle[] = $vehicle;
$vehicle->setUser($this);
}
return $this;
}
public function removeVehicle(Vehicle $vehicle): self
{
if ($this->vehicle->contains($vehicle)) {
$this->vehicle->removeElement($vehicle);
// set the owning side to null (unless already changed)
if ($vehicle->getUser() === $this) {
$vehicle->setUser(null);
}
}
return $this;
}
public function getAccount(): ?Account
{
return $this->account;
}
public function setAccount(?Account $account): self
{
$this->account = $account;
return $this;
}
}
Assuming you are using the default voter and entity security user provider.
This should apply for Symfony 3.4+, but knowing which version of Symfony you are using, would grant other approaches.
At the end of every request (unless your firewall is stateless), your
User object is serialized to the session. At the beginning of the next
request, it's deserialized and then passed to your user provider to
"refresh" it (e.g. Doctrine queries for a fresh user).
Then, the two User objects (the original from the session and the
refreshed User object) are "compared" to see if they are "equal". By
default, the core AbstractToken class compares the return values of
the getPassword(), getSalt() and getUsername() methods. If any of
these are different, your user will be logged out. This is a security
measure to make sure that malicious users can be de-authenticated if
core user data changes.
However, in some cases, this process can cause unexpected
authentication problems. If you're having problems authenticating, it
could be that you are authenticating successfully, but you immediately
lose authentication after the first redirect.
Source: https://symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session
The issue appears to be caused by
$user->setPassword($passwordEncoder->encodePassword($user,$user->getPassword()));
Which will generate a new hashed password from the submitted password, invalidating the user state, even if it is identical.
You would need to store the user's plain-text password, and validate if it has changed, and apply the password changes only if it changed.
Additionally your image form setting is not valid, since your User::$image requires a string, but the form will upload a File object (causing an invalid Entity state or calling File::__toString and changing the image). You should use a separate property for the image upload and manually draw the current image in your view or consider using a data transformer in your Form rather than in your controller to handle the state change. See: https://symfony.com/doc/current/form/data_transformers.html
Replace your current password and image form fields with the plainPassword and uploadImage fields.
class RegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//...
->add('plainPassword',RepeatedType::class,[
'type' => PasswordType::class,
'invalid_message' => 'The password fields must match.',
'required' => false,
'options' => ['attr' => ['class' => 'password-field']],
'first_options' => ['label' => 'Password','attr'=>['placeholder'=>"Password"]],
'second_options' => ['label' => 'Confirm Password','attr'=>['placeholder'=>"Confirm Password"]],
])
->add('uploadImage',FileType::class,['label'=>'Photo','required'=>false,'attr'=>['hidden'=>"hidden", 'accept'=>"image/jpeg, image/png"]]);
//...
}
You should also seriously consider using a DTO, instead of the direct User entity from Doctrine to manage your data, to prevent an invalid entity state.
Then create the properties and getter/setter methods in you User entity, to store the form values.
class User implements UserInterface
{
/**
* #var string
*/
private $plainPassword = '';
/**
* #var File|null
*/
private $uploadImage;
public function getPlainPassword(): string
{
return $this->plainPassword;
}
public function setPlainPassword(string $plainPassword): void
{
$this->plainPassword = $plainPassword;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
$this->plainPassword = null;
}
public function getUploadImage(): ?File
{
return $this->uploadImage;
}
public function setUploadImage(?File $file): void
{
$this->uploadImage = $file;
}
//...
}
Since you're using the Entity manager and the RegisterType field, you can remove the manual update queries. Since the $form->handleRequest() will be applying the changes directly to the User object. I also suggest using the Paramconverter to benefit from the entity Dependency Injection for the User object.
/**
* #Route("/{user}/update", name="update", requirements={ "user":"\d+" }, methods={"GET","POST"})
* #param User $user
* #param Request $request
* #param UserPasswordEncoderInterface $passwordEncoder
* #param UserInterface $loggedUser
* #return Response
*/
public function updateUser(User $user, Request $request, UserPasswordEncoderInterface $passwordEncoder, UrlGeneratorInterface $urlGenerator): Response
{
$loggedinUser = $this->getUser(); //helper from ControllerTrait
if ($loggedinUser && loggedinUser->getId() === $user->getId()) {
$form = $this->createForm(RegisterType::class,$user, [
'validation_groups' => ['update'],
]);
$currentImage = $user->getImage();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($file = $user->getUploadImage()) {
//this logic should be moved to the Form using a data transformer
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move(
$this->getParameter('uploads_dir'), $fileName
);
$user->setImage($fileName);
}
if ('' !== $user->getPlainPassword() && !$passwordEncoder->isPasswordValid($user->getPassword(), $user->getPlainPassword())) {
//change password only when changed
$user->setPassword($passwordEncoder->encodePassword($user, $user->getPlainPassword()));
$user->eraseCredentials();
}
$em = $this->getDoctrine()->getManager();
$em->flush();
return new RedirectResponse($urlGenerator->generate('home'));
}
return $this->render('register/update.html.twig', [
'form'=>$form->createView(),
]);
}
return new RedirectResponse($urlGenerator->generate('home'));
}
If you are using Symfony < 4.1, you will need to implement \Serializable and add the serialize and unserialize methods to your User class, otherwise your entire User object will be serialized and invalidated on any change.
class User implements UserInterface, \Serializable
{
//...
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
//$this->roles //(optional)
));
}
/** #see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
//$this->roles //(optional)
) = unserialize($serialized, array('allowed_classes' => false));
}
}
Use a temporary field to hold the plaintext password in the form pre-encoding/pre-hashing (see: https://symfony.com/doc/4.0/doctrine/registration_form.html#registration-password-max - field is called plainPassword or similar).
I suspect that there is some unexpected behaviour when setting an empty password, which might invalidate session cache (symfony stores some user data to determine, if the user must be reloaded from database, and if relevant data changed, user might be logged out). The redirection alone should definitely not logout a user.
Hopefully, this will be enough.
I have a constructor and route in my custom ProfileController
private $userManager;
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
/**
* #Route("/profile/bookings", name="profile_bookings")
*/
public function bookings()
{
$user = $this->getUser();
return $this->render('profile/bookings/bookings.html.twig', array('user'=>$user));
}
And in my template I reference
{{ user.first_name }}
But I get the error:
HTTP 500 Internal Server Error
Neither the property "first_name" nor one of the methods "first_name()", "getfirst_name()"/"isfirst_name()"/"hasfirst_name()" or "__call()" exist and have public access in class "App\Entity\User".
How do I get the user info from db and display in sub pages of profile?
Edit: User Entity ...
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\Column(type="string", length=190)
*/
private $first_name;
/**
* #ORM\Column(type="string", length=190)
*/
private $last_name;
/**
* #ORM\Column(type="string", length=190, nullable=true)
*/
private $phone_number;
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $profile_height;
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $profile_weight;
/**
* #ORM\Column(type="date", nullable=true)
*/
private $profile_dob;
/**
* #ORM\Column(type="string", length=190, nullable=true)
*/
private $profile_gender;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Booking", mappedBy="user")
*/
private $bookings;
public function __construct()
{
parent::__construct();
$this->bookings = new ArrayCollection();
}
/**
* Overridde setEmail method so that username is now optional
*
* #param string $email
* #return User
*/
public function setEmail($email)
{
$this->setUsername($email);
return parent::setEmail($email);
}
public function getFirstName()
{
return $this->first_name;
}
public function setFirstName($first_name)
{
$this->first_name = $first_name;
}
public function getLastName()
{
return $this->last_name;
}
public function setLastName($last_name)
{
$this->last_name = $last_name;
}
public function getPhoneNumber(): ?string
{
return $this->phone_number;
}
public function setPhoneNumber(string $phone_number): self
{
$this->phone_number = $phone_number;
return $this;
}
public function getProfileHeight(): ?int
{
return $this->profile_height;
}
public function setProfileHeight(?int $profile_height): self
{
$this->profile_height = $profile_height;
return $this;
}
public function getProfileDob(): ?\DateTimeInterface
{
return $this->profile_dob;
}
public function setProfileDob(?\DateTimeInterface $profile_dob): self
{
$this->profile_dob = $profile_dob;
return $this;
}
public function getProfileWeight(): ?int
{
return $this->profile_weight;
}
public function setProfileWeight(?int $profile_weight): self
{
$this->profile_weight = $profile_weight;
return $this;
}
public function getProfileGender(): ?string
{
return $this->profile_gender;
}
public function setProfileGender(?string $profile_gender): self
{
$this->profile_gender = $profile_gender;
return $this;
}
/**
* #return Collection|Booking[]
*/
public function getBookings(): Collection
{
return $this->bookings;
}
public function addBooking(Booking $booking): self
{
if (!$this->bookings->contains($booking)) {
$this->bookings[] = $booking;
$booking->setUser($this);
}
return $this;
}
public function removeBooking(Booking $booking): self
{
if ($this->bookings->contains($booking)) {
$this->bookings->removeElement($booking);
// set the owning side to null (unless already changed)
if ($booking->getUser() === $this) {
$booking->setUser(null);
}
}
return $this;
}
}
Thanks.
#Franck Gamess is right but you can also get rid of the get.
If you write {{ user.firstName }}, twig will associate that to your method getFirstName() automatically.
I don't know why you write your properties with snake_case but you could change it to camelCase and access your properties via their "real" name.
Just use in your twig template:
{{ user.getFirstName }}
It works fine. Normally what Twig does is quite simple on the PHP Layer:
check if user is an array and first_name a valid element;
if not, and if user is an object, check that first_name is a valid property;
if not, and if user is an object, check that first_name is a valid method (even if first_name is the constructor - use __construct() instead);
if not, and if user is an object, check that getfirst_name is a valid method;
if not, and if user is an object, check that isfirst_name is a valid method;
if not, and if user is an object, check that hasfirst_name is a valid method;
if not, return a null value.
See Twig variables.
By the way you should follow the Symfony Coding Standard for your variable, because it can be difficult for twig to find value of properties written in snake_case.
I don't think you should construct the UserManagerInterface in your controller. Also, like Franck says, use the coding standard if you can, it will save a lot of time and frustration in the future!
Here is the controller I use in a Symfony 4 project:
namespace App\Controller;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* #Route("/profile/bookings", name="profile_bookings")
*/
public function bookings()
{
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
return $this->render('profile/bookings/bookings.html.twig', array(
'user' => $user,
));
}
}
I'm pretty new to Symfony in general, I mostly used it because I needed to do something secure very fast, and also to discover Symfony 4.
I'm trying to make a secure connexion with the Security recipe but I'm facing two major problems (probably related) and a small one.
First, I tried to define the salt as nullable but it's still NOT NULL in db. Here's my definition of the column :
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
So now the big problems : Passwords I add are not hashed and trying to connect returns error 500
I tried to follow the documentation and here are :
My Entity
use Doctrine\ORM\Mapping as ORM;
use PhpParser\Node\Scalar\String_;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_user")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* #ORM\Column(type="string", length=255)
*/
private $password;
/**
* #ORM\Column(type="string", length=254, unique=true, nullable=true)
*/
private $email;
/**
* #ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
/**
* #ORM\Column(name="alias", type="string")
*/
private $alias;
/**
* #return mixed
*/
public function getAlias()
{
return $this->alias;
}
/**
* #param mixed $alias
*/
public function setAlias($alias): void
{
$this->alias = $alias;
}
public function __construct()
{
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid('', true));
}
public function getUsername()
{
return $this->username;
}
public function getSalt() :String
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return $this->salt;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize([
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
]);
}
/** #see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized, ['allowed_classes' => false]);
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #param mixed $email
*/
public function setEmail($email): void
{
$this->email = $email;
}
/**
* #return mixed
*/
public function getisActive()
{
return $this->isActive;
}
/**
* #param mixed $isActive
*/
public function setIsActive($isActive): void
{
$this->isActive = $isActive;
}
/**
* #param mixed $username
*/
public function setUsername($username): void
{
$this->username = $username;
}
/**
* #param mixed $password
*/
public function setPassword($password): void
{
$this->password = $password;
}
/**
* #param mixed $salt
*/
public function setSalt($salt): void
{
$this->salt = $salt;
}
}
My Controllers
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends Controller
{
/**
* #Route("/login", name="login")
*/
public function login(Request $request, AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
}
and
use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* #Route("/user")
*/
class UserController extends Controller
{
/**
* #Route("/", name="user_index", methods="GET")
*/
public function index(UserRepository $userRepository): Response
{
return $this->render('user/index.html.twig', ['users' => $userRepository->findAll()]);
}
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_show", methods="GET")
*/
public function show(User $user): Response
{
return $this->render('user/show.html.twig', ['user' => $user]);
}
/**
* #Route("/{id}/edit", name="user_edit", methods="GET|POST")
*/
public function edit(Request $request, User $user): Response
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_delete", methods="DELETE")
*/
public function delete(Request $request, User $user): Response
{
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
$em = $this->getDoctrine()->getManager();
$em->remove($user);
$em->flush();
}
return $this->redirectToRoute('user_index');
}
public function register(User $user, UserPasswordEncoderInterface $encoder)
{
$plainPassword = $user->getPassword();
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
}
and my security.yaml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: db_provider
form_login:
login_path: login
check_path: login
logout:
path: /logout
target: /homepage
pattern: ^/admin
http_basic: ~
encoders:
App\Entity\User:
algorithm: argon2i
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
I tried to add this after checking if for isSubmited and isValid in my UserController::new()
$plainPassword = $user->getPassword;
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
But I had an error Saying that the UserPasswordEncoderInterface $encoder I passed as method argument wasn't injected when loading the form. Still I'm not sure it would be a good solution to make it work as I would have to duplicate that logic in the UserController::edit(), which does not look like Symfony-like code.
(the error :)
"Controller "App\Controller\UserController::new()" requires that you provide a value for the "$encoder" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one."
I also tried to copy/paste (that how desperate I am...) the code in my UserController and then the SecurityController but this didn't work either
public function register(UserPasswordEncoderInterface $encoder)
{
// whatever *your* User object is
$user = new App\Entity\User();
$plainPassword = 'ryanpass';
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
I'm getting this as log from the server :
"No encoder has been configured for account "App\Entity\User"."
I also tried to insert directly in my db some values, but trying to connect gave me a "Access Denied" message when entering the right password, which I think is another problem...
I really don't get where I'm wrong and I couldn't find people asking about this. I'd be sincerely grateful if you could help me.
Note :
The UserController routes start with /user and is completely public as I need a user to access secured admin panel.
EDIT
I'm using MySQL 5.7 and PHP 7.2 if that can be related
Since you're using Argon2i as the encoder algorithm for your entity, your $salt becomes obsolete:
Do you need to use a Salt property?
If you use bcrypt or argon2i, no. Otherwise, yes. All passwords must be hashed with a salt, but bcrypt and argon2i do this internally [...] the getSalt() method in User can just return null (it's not used). [...]
-How to Load Security Users from the Database (the Entity Provider)
Try removing the $salt property and the setter method, and let your getSalt() return null. Persist the user without encoding operations and check the persisted password.
While this can be seen as a dirty hack, it seems to be a good practice...
I finnally found a solution thanks to #LeonWillens. Actually removing the salt property and setters made me discover that the security recipe come without the validator. So I ran composer require doctrine form security validator. I added a plainText field in my Entity which is not a column
/**
* #Assert\NotBlank()
* #Assert\Length(max=4096)
*/
private $plainPassword;
With that, I could add this logic in UserController::new()
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
I change the encoders in my security.yaml
encoders:
Symfony\Component\Security\Core\User\User: plaintext
App\Entity\User:
algorithm: argon2i
And now adding a user work perfectly. I still have problems with connexion, but no such thing as an Exception thrown
I'd like the user to register, confirm it's email, but being activated manually by an administrator.
Thanks to this page I found the FOSUserEvents::REGISTRATION_CONFIRMED which is called right after clicking on the confirmation link in the email.
Now I'd like to disable the account (see below).
class RegistrationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
}
}
Or is there any configuration that I missed?
Any ideas?
Thanks in advance!
As I can see, inside FOS\UserBundle\Controller\RegistrationController::
confirmAction() user is enabled:
/**
* Receive the confirmation token from user email provider, login the user.
*
* #param Request $request
* #param string $token
*
* #return Response
*/
public function confirmAction(Request $request, $token)
{
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
...
$user->setConfirmationToken(null);
$user->setEnabled(true);
$event = new GetResponseUserEvent($user, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRM, $event);
$userManager->updateUser($user);
...
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRMED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
I can think of two things you can do to disable it.
1) write an event listener, that will react on FOSUserEvents::REGISTRATION_CONFIRMED and disable the user => http://symfony.com/doc/master/bundles/FOSUserBundle/controller_events.html
2) override RegistrationController => https://symfony.com/doc/current/bundles/FOSUserBundle/overriding_controllers.html
I prefer first option.
class RegistrationListener implements EventSubscriberInterface
{
/** #var EntityManager */
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'onRegistrationCompleted'
);
}
public function onRegistrationCompleted(UserEvent $event) {
// registration completed
// TODO: disable the user. How?
$user = $event->getUser();
$user->setEnabled(false);
$this->em->persist($user);
$this->em->flush();
}
}
I'm on symfony 2.6.3 with stof Doctrine extension.
TimeStampable and SoftDeletable work well.
Also Blameable "on create" and "on update" are working well too:
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="createdBy", referencedColumnName="id")
*/
protected $createdBy;
/**
* #var User $updatedBy
*
* #Gedmo\Blameable(on="update")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="updatedBy", referencedColumnName="id")
*/
protected $updatedBy;
But "on change" seems not to be working.
/**
* #var User $deletedBy
*
* #Gedmo\Blameable(on="change", field="deletedAt")
* #ORM\ManyToOne(targetEntity="my\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deletedBy", referencedColumnName="id")
*/
protected $deletedBy;
I've got SoftDeletable configured on "deletedAt" field. SoftDeletable works fine, but deletedBy is never filled.
How can I manage to make it work? I just want to set user id who deleted the entity.
Here my solution :
mybundle.soft_delete:
class: Listener\SoftDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine_mongodb.odm.event_listener, event: preSoftDelete }
class SoftDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* Method called before "soft delete" system happened.
*
* #param LifecycleEventArgs $lifeCycleEvent Event details.
*/
public function preSoftDelete(LifecycleEventArgs $lifeCycleEvent)
{
$document = $lifeCycleEvent->getDocument();
if ($document instanceof SoftDeletedByInterface) {
$token = $this->tokenStorage->getToken();
if (is_object($token)) {
$oldValue = $document->getDeletedBy();
$user = $token->getUser();
$document->setDeletedBy($user);
$uow = $lifeCycleEvent->getObjectManager()->getUnitOfWork();
$uow->propertyChanged($document, 'deletedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($document, array('deletedBy' => array($oldValue, $user)));
}
}
}
}
The problem is you want to update entity (set user) when you call remove method on it.
Currently there may not be a perfect solution for registering user who soft-deleted an object using Softdeleteable + Blameable extensions.
Some idea might be to overwrite SoftDeleteableListener (https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php) but I had a problem doing it.
My current working solution is to use Entity Listener Resolver.
MyEntity.php
/**
* #ORM\EntityListeners({„Acme\MyBundle\Entity\Listener\MyEntityListener" })
*/
class MyEntity {
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deleted_by", referencedColumnName="id")
*/
private $deletedBy;
public function getDeletedBy()
{
return $this->deletedBy;
}
public function setDeletedBy($deletedBy)
{
$this->deletedBy = $deletedBy;
}
MyEntityListener.php
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Acme\MyBundle\Entity\MyEntity;
class MyEntityListener
{
/**
* #var TokenStorageInterface
*/
private $token_storage;
public function __construct(TokenStorageInterface $token_storage)
{
$this->token_storage = $token_storage;
}
public function preRemove(MyEntity $myentity, LifecycleEventArgs $event)
{
$token = $this->token_storage->getToken();
if (null !== $token) {
$entityManager = $event->getObjectManager();
$myentity->setDeletedBy($token->getUser());
$entityManager->persist($myentity);
$entityManager->flush();
}
}
}
An imperfection here is calling flush method.
Register service:
services:
myentity.listener.resolver:
class: Acme\MyBundle\Entity\Listener\MyEntityListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.orm.entity_listener, event: preRemove }
Update doctrine/doctrine-bundle in composer.json:
"doctrine/doctrine-bundle": "1.3.x-dev"
If you have any other solutions, especially if it is about SoftDeleteableListener, please post it here.
This is my solution, I use preSoftDelete event:
app.event.entity_delete:
class: AppBundle\EventListener\EntityDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.event_listener, event: preSoftDelete, connection: default }
and service:
<?php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EntityDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function preSoftDelete(LifecycleEventArgs $args)
{
$token = $this->tokenStorage->getToken();
$object = $args->getEntity();
$om = $args->getEntityManager();
$uow = $om->getUnitOfWork();
if (!method_exists($object, 'setDeletedBy')) {
return;
}
if (null == $token) {
throw new AccessDeniedException('Only authorized users can delete entities');
}
$meta = $om->getClassMetadata(get_class($object));
$reflProp = $meta->getReflectionProperty('deletedBy');
$oldValue = $reflProp->getValue($object);
$reflProp->setValue($object, $token->getUser()->getUsername());
$om->persist($object);
$uow->propertyChanged($object, 'deletedBy', $oldValue, $token->getUser()->getUsername());
$uow->scheduleExtraUpdate($object, array(
'deletedBy' => array($oldValue, $token->getUser()->getUsername()),
));
}
}
It's not consistence because I check setDeletedBy method exists and set deletedBy property, but it work for me, and you can upgrade this code for your needs
Here is another solution I found :
Register a service:
softdeleteable.listener:
class: AppBundle\EventListener\SoftDeleteableListener
arguments:
- '#security.token_storage'
tags:
- { name: doctrine.event_listener, event: preFlush, method: preFlush }
SoftDeleteableListener:
/**
* #var TokenStorageInterface|null
*/
private $tokenStorage;
/**
* DoctrineListener constructor.
*
* #param TokenStorageInterface|null $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event)
{
$user = $this->getUser();
$em = $event->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $object) {
/** #var SoftDeleteableEntity|BlameableEntity $object */
if (method_exists($object, 'getDeletedBy') && $user instanceof User) {
$object->setDeletedBy($user);
$em->merge($object);
// Persist and Flush allready managed by other doctrine extensions.
}
}
}
/**
* #return User|void
*/
public function getUser()
{
if (!$this->tokenStorage || !$this->tokenStorage instanceof TokenStorageInterface) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
$token = $this->tokenStorage->getToken();
if (!$token) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
$user = $token->getUser();
if (!$user instanceof User) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
return $user;
}