PUGX / PUGXMultiUserBundle

An extension for FOSUserBundle to handle users of different types. Compatible with Doctrine ORM.
163 stars 96 forks source link

Logging in a user directly in code gives a "no user provider for user" error #68

Open jonben opened 10 years ago

jonben commented 10 years ago

Great work on this plugin. I have 4 different types of users in my app and it works fine except this issue:

After a user registers, I would like to automatically log him in.

With FOSUserBundle I can log in a user like this :

$user = $em->getRepository('MyAppUser2Bundle:User2')->findOneByUsername('testuser');
$this->container->get('fos_user.security.login_manager')->loginUser(
                $this->container->getParameter('fos_user.firewall_name'), $user);

Except when I do this with a PUGXMultiUserBundle user, I get this error :

There is no user provider for user "MyApp\User2Bundle\Entity\User2". vendor\symfony\symfony\src\Symfony\Component\Security\Http\Firewall\ContextListener.php at line 177

I have in security.yml:

security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

And MyApp\User2Bundle\Entity\User2 class extends a User class that itself extends FOS\UserBundle\Model\User, so I don't think I need a special user provider...

Any clue what I got wrong here?

EDIT : actually when I try to login a user from the first type of user (in my case UserAdmin) it works, but it doesn't for the other 3 types of users.

Of what I find, the User Provider's class is not the base PUGXMultiUserBundle class that extends FOS\UserBundle\Model\User but the class of the first user defined in config.yml (if I switch the users, I can only log in this way with the first type of user).

brunonic commented 9 years ago

Same here, any solution ?

modnarlluf commented 9 years ago

I had to deal with the same problem. After some investigation I got this !

TLDR: You have to persist the user class that you want to use in the UserDiscriminator when you manually log the user. In exemple:

$discriminator = $this->get('pugx_user.manager.user_discriminator');

// the 2nd parameter at true is the key
$discriminator->setClass('MyApp\User2Bundle\Entity\User2', true);

$user = $em->getRepository('MyAppUser2Bundle:User2')->findOneByUsername('testuser');
$this->container->get('fos_user.security.login_manager')->loginUser(
     $this->container->getParameter('fos_user.firewall_name'),
     $user
);

Detailed explanation: First, sorry if my english is not that good. I assume that you try to redirect the user after you log in him.

The SF2 ContextListener refresh the current user from the session for each (master?) requests. To do so, he loops over all providers registered (only fos_userbundle here) and call the refreshUser() method for each provider.

Each UserProvider must implements the SF2 UserProviderInterface, which force the implementation of the methods refreshUser() and supportsClass().

The FOS UserProvider call his own supportsClass():

// [...]/vendor/friendsofsymfony/user-bundle/Security/UserProvider.php
public function supportsClass($class)
{
    $userClass = $this->userManager->getClass();

    return $userClass === $class || is_subclass_of($class, $userClass);
}

We see that it calls the getClass() method from the UserManager, which is the PUGX UserManager in this situation.

// [...]/vendor/pugx/multi-user-bundle/PUGX/MultiUserBundle/Doctrine/UserManager.php
public function getClass()
{
    return $this->userDiscriminator->getClass();
}

The UserDiscriminator is particular to the PUGXMultiUserBundle. We're close to the end. :)

// [...]/vendor/pugx/multi-user-bundle/PUGX/MultiUserBundle/Model/UserDiscriminator.php
public function getClass()
{
    if (!is_null($this->class)) {
        return $this->class;
    }

    $storedClass = $this->session->get(static::SESSION_NAME, null);

    if ($storedClass) {
        $this->class = $storedClass;
    }

    if (is_null($this->class)) {
        $entities = $this->getClasses();
        $this->class = $entities[0];
    }

    return $this->class;
}

public function setClass($class, $persist = false)
{
    if (!in_array($class, $this->getClasses())) {
        throw new \LogicException(sprintf('Impossible to set the class discriminator, because the class "%s" is not present in the entities list', $class));
    }

    if ($persist) {
        $this->session->set(static::SESSION_NAME, $class);
    }

    $this->class = $class;
}

OK. There we are. The getClass() method check if the user class has been set previously (via the setClass()). But we are on the ContextListener, on a new request, so no way to have use it.

Then it tries to retrieve the user class from the session. This is the main point: You can save the user class that you want to use in session. To do so, you just have to call the setClass() method with true as second argument. You can't call it from the ContextListener either, but you can call it when you manually log your user (in your controller):

$discriminator = $this->get('pugx_user.manager.user_discriminator');

// the 2nd parameter at true is the key
$discriminator->setClass('MyApp\User2Bundle\Entity\User2', true);

$user = $em->getRepository('MyAppUser2Bundle:User2')->findOneByUsername('testuser');
$this->container->get('fos_user.security.login_manager')->loginUser(
     $this->container->getParameter('fos_user.firewall_name'),
     $user
);

This way, for the next requests, the UserDiscriminator::getClass() method will retrieve the user class from the session. At this point, the ContextListener will no longer have difficulties to refresh your user.

If you check the last part of the UserDiscriminator::getClass() method, then you will notice that if no user class is found, it take the first user class defined in your config.yml. This explains why you could log in only the first user defined in your config.yml.