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
I have set the relationship on the entity's to set many customers to a user entity as a collection and added Multiple to the form field...it's posting ok it's just not updating the user_id in the customer table but it was when using OneToOne relation. Any help would be appreciated.
User entity code
/**
* #var Customer[]
* #ORM\OneToMany(targetEntity="App\Entity\Customer", mappedBy="user", cascade={"all"})
* #ORM\JoinColumn(nullable=true)
*/
private $customer;
public function __construct()
{
$this->staffUsers = new ArrayCollection();
$this->customer = new ArrayCollection();
}
/**
* #param Collection|null $customer
* #return $this
*/
public function setCustomer(?Collection $customer): self
{
$this->customer = $customer;
return $this;
}
Customer entity code
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="customer", cascade={"all"})
*/
private $user;
/**
* #return User|null
*/
public function getUser(): ?User
{
return $this->user;
}
/**
* #param User|null $user
* #return $this
*/
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
Controller code
public function newUser(Request $request, UserPasswordEncoderInterface $encoder) : Response
{
/** #var UserRepository $userRepo */
$userRepo = $this->getDoctrine()->getRepository(User::class);
$customer = new Customer();
// make form
$form = $this->createForm(UserType::class,new User());
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
/** #var User $newUser */
$newUser = $form->getData();
// dump($newUser);
// die();
// hold user roles
$roles = ['ROLE_USER'];
// check if admin role
$adminRole = (bool)$form->get('adminRole')->getData();
if($adminRole){
$roles[]='ROLE_ADMIN';
}
// is a customer selected?
if($newUser->getCustomer() && $newUser->getCustomer()->count() > 0){
$roles[]='ROLE_CUSTOMER';
}
$newUser->setRoles($roles);
// encode pw
$newUser->setPassword(
$encoder->encodePassword($newUser,$newUser->getPassword())
);
// create
$userRepo->insert($newUser);
return $this->redirectToRoute('usersListing');
}
return $this->render('admin/users/user-form.html.twig',[
'form'=>$form->createView()
]);
}
Customer entity type on User form
->add('customer',EntityType::class,[
'required'=>false,
'multiple' => true,
'attr'=>[
'class'=>'selectpicker form-control',
'multiple' =>'multiple',
'data-width' => "100%"
],
'label'=>'Customer(s)',
'placeholder'=>'N/A',
'class'=>Customer::class,
'query_builder'=>function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.lname', 'ASC')
->orderBy('c.fname','ASC');
},
'constraints'=>[
new Callback(function(?Collection $customers, ExecutionContextInterface $context) use($userRepo){
// check if the customer is already linked to a user
if($customers && $customers->count() > 0){
/** #var Customer $customer */
foreach($customers as $customer){
if($customer->getUser()){
$context->addViolation('Customer Is Already Linked To User: ' . $customer->getUser()->getUsername());
return false;
}
}
}
return true;
})
]
])
Rename property customer to customers and function from setCustomer to setCustomers, you should also create an addCustomer method in your User class:
public function addCustomer(Customer $customer)
{
$this->customers[] = $customer;
$customer->setUser($this); // sets the owning side, without this your will end up with user_id equal to null
}
And whenever you want to add a customer you just invoke the addCustomer method.
If you want to use the setCustomers method make sure you set the user in your customer entity.
Symfony documentation mentions that there should be two Password properties(password and plainPassword). The plainPassword is only used to get the password that a user typed(e.g via a registration form) and is not persisted by doctrine(hence not stored in the database). Password property on the other hand is set after encrypting the plainPassword. Is it not possible to use the same property (password) to collect the plainPassword from the user to avoid having two password properties?
Here is my controller code:
class SecurityController extends Controller
{
/**
* #Route("/register", name="security_register")
*/
public function register(Request $request,
UserPasswordEncoderInterface $passwordEncode)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData();
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('admin');
}
return $this->render(
'security/register.html.twig',
array('form' => $form->createView())
);
}
And here my User Entity:
class User implements UserInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
private $email;
/**
* #ORM\Column(type="string", unique=true)
* #Assert\NotBlank()
*/
private $username;
/**
* #Assert\NotBlank()
* #Assert\Length(max=4096)
*/
private $plainPassword;
/**
* The below length depends on the "algorithm" you use for encoding
* the password, but this works well with bcrypt.
*
* #ORM\Column(type="string", length=64)
*/
private $password;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getSalt()
{
// The bcrypt and argon2i algorithms don't require a separate salt.
// You *may* need a real salt if you choose a different encoder.
return null;
}
// other methods, including security methods like getRoles()
public function getRoles()
{
return array('ROLE_ADMIN');
}
public function eraseCredentials()
{
}
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$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]);
}
}
The only reason Symfony suggests to use a plainPassword field is that by using directly the password field in your registration form, you could end up with an unencrypted password in your database in case you don't implement correctly the encryption procedure before storing the user. And that would be a huge security issue.
Im creating a WebBrowser game with Symfony2. What I want to achieve is:
I have a table with Users. When new user registers in the game, new record is added to table fos_user. When new user is registered I also want to put records in the table that stores users resources in the game with starting quantity.
I have read about event listeners but I'm not sure if they are the best way to resolve my problem.
This is the Entity that holds User, type of material and its quantity
/**
* #var int
*
* #ORM\Column(name="quantity", type="bigint")
*/
private $quantity;
/*
* connection material->MaterialStock<-User
*/
/**
*#ORM\ManyToOne(targetEntity="Material", inversedBy="userMaterial")
*
*/
private $material;
/**
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="userMaterial")
*/
private $user;
function getId() {
return $this->id;
}
function getQuantity() {
return $this->quantity;
}
function getMaterial() {
return $this->material;
}
function getUser() {
return $this->user;
}
function setQuantity($quantity) {
$this->quantity = $quantity;
}
function setMaterial($material) {
$this->material = $material;
}
function setUser($user) {
$this->user = $user;
}
}
User entity looks like this
<?php
// src/AppBundle/Entity/User.php
namespace FactoryBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use FactoryBundle\Entity\Factory;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*
*/
protected $id;
public function __construct()
{
parent::__construct();
$this->productionOrders = new ArrayCollection();
}
/**
* #ORM\OneToOne(targetEntity="Factory", mappedBy="user")
*/
private $factory;
/*
* connecting User->materialStock<-Material
*/
/**
*
* #ORM\OneToMany(targetEntity="MaterialStock", mappedBy="user")
*/
private $userMaterial;
/**
* #ORM\OneToMany(targetEntity="ProductionOrders", mappedBy="user")
*/
private $productionOrders;
/** #ORM\OneToMany(targetEntity="ToyStock", mappedBy="user") */
private $userToyStock;
function getId() {
return $this->id;
}
function getFactory() {
return $this->factory;
}
function getUserMaterial() {
return $this->userMaterial;
}
function getProductionOrders() {
return $this->productionOrders;
}
function getUserToyStock() {
return $this->userToyStock;
}
function setId($id) {
$this->id = $id;
}
function setFactory($factory) {
$this->factory = $factory;
}
function setUserMaterial($userMaterial) {
$this->userMaterial = $userMaterial;
}
function setProductionOrders($productionOrders) {
$this->productionOrders = $productionOrders;
}
function setUserToyStock($userToyStock) {
$this->userToyStock = $userToyStock;
}
}
You can use an event subscriber.
<?php
namespace ...;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use Steora\Api\UserBundle\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use ...\EntityThatHoldsUserTypeOfMaterialAndQuantity;
/**
* ...
*/
class RegistrationCompletionListener implements EventSubscriberInterface
{
/** #var EntityManager */
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_COMPLETED => 'onRegistrationCompletionSuccess',
);
}
/**
* #param FilterUserResponseEvent $event
*/
public function onRegistrationCompletionSuccess(FilterUserResponseEvent $event)
{
// you can modify response here, but you can remove this line if there is no need to touch response...
$response = $event->getResponse();
$user = $event->getUser();
$entityThatHoldsUserTypeOfMaterialAndQuantity = new EntityThatHoldsUserTypeOfMaterialAndQuantity();
$entityThatHoldsUserTypeOfMaterialAndQuantity->setUser($user);
$entityThatHoldsUserTypeOfMaterialAndQuantity->setQuantity(...);
...
$this->em->persist($entityThatHoldsUserTypeOfMaterialAndQuantity);
$this->em->flush();
}
}
Register your service in service.yml
services:
...
steora.api.user.registration_confirmation:
class: YourBundle\..\RegistrationCompletionListener
arguments: ['#doctrine.orm.default_entity_manager']
tags:
- { name: kernel.event_subscriber }
So, you are listening for some event, and when that event happens, you do stuff that you need :)
Here you can find more events, maybe some other than FOSUserEvents::REGISTRATION_COMPLETED is more suitable for you: https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/FOSUserEvents.php
Here is an example from the official docs: http://symfony.com/doc/current/bundles/FOSUserBundle/controller_events.html
This is workflow:
1) A user is filling a registration form and submits his data.
2) If the form is valid, this is what happens:
// friendsofsymfony/user-bundle/Controller/RegistrationController.php
if ($form->isSubmitted()) {
if ($form->isValid()) {
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);
$userManager->updateUser($user);
if (null === $response = $event->getResponse()) {
$url = $this->generateUrl('fos_user_registration_confirmed');
$response = new RedirectResponse($url);
}
// This is event you are listening for!!!!
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));
return $response;
}
$event = new FormEvent($form, $request);
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event);
if (null !== $response = $event->getResponse()) {
return $response;
}
}
3) Your listener reacts on event, and in onRegistrationCompletionSuccess() you do your stuff, and after that everything continues as usual :)
Matko Đipalo
Thank you for your answer. If I understood you correctly the workflow of your approach is:
FOSUser:RegistrationController.php creates an event when registration is completed
$dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));
in your class RegistrationCompletionListener in line:
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_COMPLETED => 'onRegistrationCompletionSuccess',
);
}enter code here
Im specifieing to what events I want my app to listen too and on line:
public function onRegistrationCompletionSuccess(FilterUserResponseEvent $event)
{
$response = $event->getResponse();
//... make needed actions
$this->yourDependencies->doSomeStufff()...
}
I can tell me script what to do when that event will accour. In my case get the user object and create for him records in database.
I have an OAuth API that requires an username and a password to get the user object (resource owner password credentials flow). I'm trying to get this end result :
User enters username/password
Symfony exchanges username/password for access and refresh tokens, then fetches the User object and populates a token with the fetched object
User is now authenticated on the website
The issue that I'm having is that I cannot seem to figure out how to do it the best way I can see : with an User provider. The UserProviderInterface asks to implement loadUserByUsername(), however I cannot do that, as I need the username AND the password to fetch the user object.
I tried to implement the SimplePreAuthenticatorInterface, but I still run into the same issue: after creating the PreAuthenticated token in createToken(), I need to authenticate it using authenticateToken(), and I still cannot fetch the user through the UserProvider, since I first have to use the username/password to get an access token that'd allow me to fetch the User object. I thought about adding a method to login in my UserProvider that'd login through the API using username/password and store the logged in tokens for any username in an array, and then fetch the tokens by username in that array, but that doesn't feel right.
Am I looking at it from the wrong angle ? Should I not be using PreAuthenticated tokens at all ?
A while ago i needed to implement a way to authenticate users through a webservice. This is what i end up doing based on this doc and the form login implementation of the symfony core.
First create a Token that represents the User authentication data present in the request:
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WebserviceAuthToken extends AbstractToken
{
/**
* The password of the user.
*
* #var string
*/
private $password;
/**
* Authenticated Session ID.
*
* #var string
*/
private $authSessionID;
public function __construct($user, $password, array $roles = array())
{
parent::__construct($roles);
$this->setUser($user);
$this->password = $password;
parent::setAuthenticated(count($roles) > 0);
}
/**
* {#inheritDoc}
*/
public function getCredentials()
{
return '';
}
/**
* Returns the Authenticated Session ID.
*
* #return string
*/
public function getAuthSessionID()
{
return $this->authSessionID;
}
/**
* Sets the Authenticated Session ID.
*
* #param string $authSessionID
*/
public function setAuthSessionID($authSessionID)
{
$this->authSessionID = $authSessionID;
}
/**
* Returns the Password used to attempt login.
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
/**
* {#inheritDoc}
*/
public function serialize()
{
return serialize(array(
$this->authSessionID,
parent::serialize()
));
}
/**
* {#inheritDoc}
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
list(
$this->authSessionID,
$parent,
) = $data;
parent::unserialize($parent);
}
}
The AuthSessionID that im storing is a token returned from the webservice that allows me to perform requests as an authenticated user.
Create a Webservice authentication listener which is responsible for fielding requests to the firewall and calling the authentication provider:
use RPanelBundle\Security\Authentication\Token\RPanelAuthToken;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class WebserviceAuthListener extends AbstractAuthenticationListener
{
private $csrfTokenManager;
/**
* {#inheritdoc}
*/
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, $csrfTokenManager = null)
{
if ($csrfTokenManager instanceof CsrfProviderInterface) {
$csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager);
} elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) {
throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.');
}
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
}
/**
* {#inheritdoc}
*/
protected function requiresAuthentication(Request $request)
{
if ($this->options['post_only'] && !$request->isMethod('POST')) {
return false;
}
return parent::requiresAuthentication($request);
}
/**
* {#inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if (null !== $this->csrfTokenManager) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
if ($this->options['post_only']) {
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
} else {
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new WebserviceAuthToken($username, $password));
}
}
Create a Webservice login factory where we wook into the Security Component, and tell which is the User Provider and the available options:
class WebserviceFormLoginFactory extends FormLoginFactory
{
/**
* {#inheritDoc}
*/
public function getKey()
{
return 'webservice-form-login';
}
/**
* {#inheritDoc}
*/
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'app.security.authentication.provider.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('app.security.authentication.provider'))
->replaceArgument(1, new Reference($userProviderId))
->replaceArgument(2, $id);
return $provider;
}
/**
* {#inheritDoc}
*/
protected function getListenerId()
{
return 'app.security.authentication.listener';
}
}
Create an Authentication provider that will verify the validaty of the WebserviceAuthToken
class WebserviceAuthProvider implements AuthenticationProviderInterface
{
/**
* Service to handle DMApi account related calls.
*
* #var AccountRequest
*/
private $apiAccountRequest;
/**
* User provider service.
*
* #var UserProviderInterface
*/
private $userProvider;
/**
* Security provider key.
*
* #var string
*/
private $providerKey;
public function __construct(AccountRequest $apiAccountRequest, UserProviderInterface $userProvider, $providerKey)
{
$this->apiAccountRequest = $apiAccountRequest;
$this->userProvider = $userProvider;
$this->providerKey = $providerKey;
}
/**
* {#inheritdoc}
*/
public function authenticate(TokenInterface $token)
{
// Check if both username and password exist
if (!$username = $token->getUsername()) {
throw new AuthenticationException('Username is required to authenticate.');
}
if (!$password = $token->getPassword()) {
throw new AuthenticationException('Password is required to authenticate.');
}
// Authenticate the User against the webservice
$loginResult = $this->apiAccountRequest->login($username, $password);
if (!$loginResult) {
throw new BadCredentialsException();
}
try {
$user = $this->userProvider->loadUserByWebserviceResponse($loginResult);
// We dont need to store the user password
$authenticatedToken = new WebserviceAuthToken($user->getUsername(), "", $user->getRoles());
$authenticatedToken->setUser($user);
$authenticatedToken->setAuthSessionID($loginResult->getAuthSid());
$authenticatedToken->setAuthenticated(true);
return $authenticatedToken;
} catch (\Exception $e) {
throw $e;
}
}
/**
* {#inheritdoc}
*/
public function supports(TokenInterface $token)
{
return $token instanceof WebserviceAuthToken;
}
}
And finally create a User provider. In my case after i receive the response from the webservice, i check if the user is stored on redis, and if not i create it. After that the user is always loaded from redis.
class WebserviceUserProvider implements UserProviderInterface
{
/**
* Wrapper to Access the Redis.
*
* #var RedisDao
*/
private $redisDao;
public function __construct(RedisDao $redisDao)
{
$this->redisDao = $redisDao;
}
/**
* {#inheritdoc}
*/
public function loadUserByUsername($username)
{
// Get the UserId based on the username
$userId = $this->redisDao->getUserIdByUsername($username);
if (!$userId) {
throw new UsernameNotFoundException("Unable to find an UserId identified by Username = $username");
}
if (!$user = $this->redisDao->getUser($userId)) {
throw new UsernameNotFoundException("Unable to find an User identified by ID = $userId");
}
if (!$user instanceof User) {
throw new UnsupportedUserException();
}
return $user;
}
/**
* Loads an User based on the webservice response.
*
* #param \AppBundle\Service\Api\Account\LoginResult $loginResult
* #return User
*/
public function loadUserByWebserviceResponse(LoginResult $loginResult)
{
$userId = $loginResult->getUserId();
$username = $loginResult->getUsername();
// Checks if this user already exists, otherwise we need to create it
if (!$user = $this->redisDao->getUser($userId)) {
$user = new User($userId, $username);
if (!$this->redisDao->setUser($user) || !$this->redisDao->mapUsernameToId($username, $userId)) {
throw new \Exception("Couldnt create a new User for username = $username");
}
}
if (!$user instanceof User) {
throw new UsernameNotFoundException();
}
if (!$this->redisDao->setUser($user)) {
throw new \Exception("Couldnt Update Data for for username = $username");
}
return $this->loadUserByUsername($username);
}
/**
* {#inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'AppBundle\Entities\User';
}
}
Required services :
app.security.user.provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ["#app.dao.redis"]
app.security.authentication.provider:
class: AppBundle\Security\Authentication\Provider\WebserviceAuthProvider
arguments: ["#api_caller", "", ""]
app.security.authentication.listener:
class: AppBundle\Security\Firewall\WebserviceAuthListener
abstract: true
parent: security.authentication.listener.abstract
Configured security:
security:
providers:
app_user_provider:
id: app.security.user.provider
firewalls:
default:
pattern: ^/
anonymous: ~
provider: app_user_provider
webservice_form_login: # Configure just like form_login from the Symfony core
If you have any question please let me know.
Entity/User
namespace My\SampleBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends \FOS\UserBundle\Entity\User
{
/** #ORM\Id #ORM\Column(type="integer") #ORM\GeneratedValue(strategy="AUTO") */
protected $id;
/**
* #ORM\OneToOne(targetEntity="Invitation", inversedBy="user")
* #ORM\JoinColumn(referencedColumnName="code")
* #Assert\NotNull(message="Your invitation is wrong")
*/
protected $invitation;
public function setInvitation(Invitation $invitation)
{
$this->invitation = $invitation;
}
public function getInvitation()
{
return $this->invitation;
}
}
Entity/Invitation
namespace My\SampleBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Entity */
class Invitation
{
/** #ORM\OneToOne(targetEntity="User", mappedBy="invitation", cascade={"persist", "merge"}) */
protected $user;
/** #ORM\Id #ORM\Column(type="string", length=6) */
protected $code;
/** #ORM\Column(type="string", length=256) */
protected $email;
/**
* When sending invitation be sure to set this value to `true`
*
* It can prevent invitations from being sent twice
*
* #ORM\Column(type="boolean")
*/
protected $sent = false;
public function __construct()
{
// generate identifier only once, here a 6 characters length code
$this->code = substr(md5(uniqid(rand(), true)), 0, 6);
}
public function getCode()
{
return $this->code;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
public function isSent()
{
return $this->sent;
}
public function send()
{
$this->sent = true;
}
public function getUser()
{
return $this->user;
}
public function setUser(User $user)
{
$this->user = $user;
}
/**
* Set code
*
* #param string $code
* #return Invitation
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* Set sent
*
* #param boolean $sent
* #return Invitation
*/
public function setSent($sent)
{
$this->sent = $sent;
return $this;
}
/**
* Get sent
*
* #return boolean
*/
public function getSent()
{
return $this->sent;
}
}
Error
You cannot search for the association field
'My\SampleBundle\Entity\Invitation#user', because it is the inverse
side of an association. Find methods only work on owning side
associations. 500 Internal Server Error - ORMException
I am the stage which just performed the documentation.
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/doc/adding_invitation_registration.md
The display of the bundle is normal. However, It has become an error if I submit registration form.
Any help?
EDIT:
Actually, I had set up 'inversedBy' at first.
A pre-question.
Symfony2 FOSUserBundle Invitation : 'inversedBy' mapping errors
On the surface, it does work. However, mapping errors is displayed by profiler.
My\SampleBundle\Entity\Invitation# does not contain the required
'inversedBy' attribute.
so, I changed it in response to advice.
I don't know what to think of it.
It's just as the error says. Instead of mappedBy, you should use inversedBy on the Invitation entity and use mappedBy on the User entity for $invitation.
/** #ORM\OneToOne(targetEntity="User", inversedBy="invitation", cascade={"persist", "merge"}) */
protected $user;
You can also overcome this problem by creating a custom repository method to find user based on invitation.
There was solution.
https://github.com/FriendsOfSymfony/FOSUserBundle/issues/800
public function reverseTransform($value)
{
// ...
return $this->entityManager
->getRepository('My\SampleBundle\Entity\Invitation')
->findOneBy(array(
'code' => $value,
'user' => null, <= Removing 'user' solves the issue
));
}
I ended up modifying my transformer to the following:
public function reverseTransform($value)
{
if (null === $value || '' === $value) {
return null;
}
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
$invitation = $this->entityManager
->getRepository('SixString\PearBundle\Entity\Invitation')
->findOneBy(array(
'code' => $value,
));
if($this->entityManager->getRepository('SixString\PearBundle\Entity\User')->findOneBy(array("invitation" => $invitation))){
return null;
}
return $invitation;
}
I stripped out the 'user' => null but added a check to see if the invitation has already been used