Symfony2: Using FOSUserBundle with multiple EntityManagers
Last week, we were looking to setup one of our Symfony2 projects to use a master/slave MySQL configuration. We’d looked into using the MasterSlaveConnection Doctrine2 connection class, but unfortunately it doesn’t really work the way you’d expect. Anyway, the “next best” way to set up master/slave connections seemed to be creating two separate EntityManagers, one pointing at the master and one at the slave. Setting up the Doctrine configurations for this is pretty straightforward, you’ll end up with YAML that looks like:
# Doctrine Configuration
doctrine:
dbal:
connections:
default:
driver: %database_driver%
host: %database_host%
port: %database_port%
dbname: %database_name%
user: %database_user%
password: %database_password%
charset: UTF8
slave:
driver: %database_driver%
host: %database_slave_host%
port: %database_slave_port%
dbname: %database_slave_name%
user: %database_slave_user%
password: %database_slave_password%
charset: UTF8
orm:
auto_generate_proxy_classes: %kernel.debug%
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
SetfiveBundle:
dir: Entity
FOSUserBundle: ~
slave:
connection: slave
mappings:
SetfiveBundle:
dir: Entity
FOSUserBundle: ~
At face value, it looked like everything was working fine but it turns out they weren’t - the FOSUserBundle entities weren’t getting properly setup on the slave connection. Turns out, because FOSUserBundle uses Doctrine2 superclasses to setup it’s fields there’s no way to natively use FOSUserBundle with multiple entity managers. The key issue is that since the UserProvider checks the class of a user being refreshed, you can’t just copy the FOSUserBundle fields directly into your entity:
<?php
// From https://github.com/FriendsOfSymfony/FOSUserBundle/blob/6fc28e41805868c6c635de12266c40595c2e7ce8/Security/UserProvider.php
public function refreshUser(SecurityUserInterface $user)
{
if (!$user instanceof User && !$user instanceof PropelUser) {
throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user)));
}
if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
}
return $reloadedUser;
}
So how do you get around this? Turns out, you need to add a custom UserProvider to bypass the instance class check. My UserProvider ended up looking like:
<?php
namespace Setfive\AcmeBundle\Security;
use FOS\UserBundle\Security\UserProvider as BaseUserProvider;
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;
class UserProvider extends BaseUserProvider {
/**
* Constructor.
*
* @param UserManagerInterface $userManager
*/
public function __construct($userManager)
{
parent::__construct($userManager);
}
public function refreshUser(SecurityUserInterface $user)
{
if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
}
return $reloadedUser;
}
}
And then the additional YAML configurations you need are:
# services.yml
setfive.security.userProvider:
class: Setfive\AcmeBundle\Security\UserProvider
arguments: [@fos_user.user_manager]
# security.yml
providers:
fos_userbundle:
id: fos_user.user_provider.username
setfive_user:
id: setfive.security.userProvider
The last step is copying all the FOSUserBundle fields directly into your User entity and update it to not extend the FOSUserBundle base class. Anyway, that’s it - two EntityManagers and one FOSUserBundle.
Questions or comments about this? Email us at contact@setfive.com.