sumocoders/framework-multi-user-bundle
Symfony bundle from SumoCoders that adds multi-user support to the framework, letting you manage multiple user accounts, associate users with shared entities or tenants, and integrate user handling into your application's authentication and admin flows.
This is a Symfony bundle (not Laravel), which explains part of why it is 1 star and archived: it was built for the SumoCoders mini-framework on Symfony 2.x/3.x and FOSUserBundle. If you are coming from Laravel, the closest mental model is "multi-auth / multi-guard": several distinct user entities, each with its own provider, firewall, login form, and user checker, all coordinated by a shared discriminator.
Minimal steps to get it running:
composer require sumocoders/framework-multi-user-bundle
// app/AppKernel.php (Symfony 2/3 style)
$bundles = [
// ...
new FOS\UserBundle\FOSUserBundle(),
new SumoCoders\FrameworkMultiUserBundle\FrameworkMultiUserBundle(),
];
config.yml. This is the file you will live in. Each type points to a concrete entity extending the bundle's base user and to its provider/firewall:
framework_multi_user:
user_discriminator: multi_user_discriminator
users:
customer:
class: AppBundle\Entity\Customer
admin:
class: AppBundle\Entity\Admin
security:
providers:
customer: { id: app.customer_provider }
admin: { id: app.admin_provider }
firewalls:
customer:
pattern: ^/customer
provider: customer
form_login: { login_path: /customer/login, check_path: /customer/login_check }
logout: { path: /customer/logout }
admin:
pattern: ^/admin
provider: admin
form_login: { login_path: /admin/login, check_path: /admin/login_check }
logout: { path: /admin/logout }
Where to look first:
DependencyInjection/Configuration.php — the actual shape of the framework_multi_user tree; this is the source of truth, not the README.UserDiscriminator (the request/listener class) — it stores the current user type in the session; nearly every bug traces back to it.Resources/config/services.xml — service ids you can override if you need to change the discriminator.Entity/ directory for the base user class you must extend.First use case: two login flows behind two firewalls (e.g. /customer/login and /admin/login), each authenticating a different Doctrine entity, with a shared base user providing roles and password hashing. That is the whole point of the bundle.
One base entity, one concrete per type. Extend the bundle's base user (which itself extends the FOS user) and let the discriminator handle routing between them:
// src/AppBundle/Entity/Admin.php
class Admin extends BaseUser
{
// admin-specific fields
}
Use Doctrine single-table inheritance on a common parent if you want a shared table, or separate tables with separate repositories if you want clean isolation. The bundle works with either, but shared identity across types is not supported — different firewalls mean different sessions of users.
Discriminator-in-session pattern. The discriminator is set when a login is attempted and read by listeners later in the request cycle. This is the pattern you'll see throughout:
// typically handled for you by the bundle's listener
$discriminator->setUserType('admin');
Provider-per-type. Each user type needs its own provider wired to its own repository. Don't reuse one provider across two firewalls — the bundle resolves the user type from the firewall, and a shared provider makes the resolution ambiguous:
services:
app.customer_provider:
class: AppBundle\Security\CustomerProvider
arguments: ['@doctrine']
User checker per provider. If your entity implements AdvancedUserInterface (locked/expired), configure a user_checker per firewall. This is where per-type business rules (e.g. "customers must be email-verified, admins must exist") belong.
Per-type forms and controllers. Register separate FOSUserBundle form types and copy the standard FOS controllers into your app, one per type. The bundle gives you the plumbing; the controllers are still yours. A common layout:
src/AppBundle/Controller/Customer/{SecurityController,RegistrationController,ProfileController}.php
src/AppBundle/Controller/Admin/{SecurityController,RegistrationController}.php
Integration tips:
^/admin, ^/customer) — the discriminator derives type from the firewall pattern, so overlapping patterns silently break login.app.admin.user_manager) rather than relying on FOSUserBundle's default manager, which assumes one user class.This package is archived and last released 2018-11-27. There will be no security patches. Treat it as a reference implementation to copy, not a dependency to run in production. The Symfony ecosystem moved to multi-guard natively long ago; FOSUserBundle itself is deprecated in favour of symfony/security-bundle's user providers. If you are on a modern stack, implement the pattern directly — don't install this.
Modern PHP install failures. The composer constraints target PHP 5.x/7.0-era. You will likely need --ignore-platform-reqs or --no-scripts to even install it. That is a warning sign, not a workaround to standardise.
The discriminator is session-backed. Two failures are extremely common:
/admin while already logged into /customer in the same session can overwrite the discriminator; always logout or verify with a clean session when debugging.app/config/services.yml priority.Firewall pattern overlap is silent. ^/customer also matches ^/customer/admin if you forgot a more specific firewall. The first firewall in order wins and the discriminator gets the wrong type. Order firewalls most-specific-first.
Doctrine inheritance pitfalls. With single-table inheritance, every query on the parent hydrates all types; if the bundle's provider filters by instanceof, that is fine, but custom DQL that joins the parent will surprise you. Add a discriminatorColumn and keep it stable — changing it later orphans rows.
Provider and user checker must agree. A user checker for customer running on the admin firewall produces "account is locked" failures for perfectly valid admins. Audit security.firewalls.*.user_checker line by line.
Debugging login loops. The chain is: firewall → provider → loadUserByUsername → user checker → discriminator. If you get a redirect back to the login form with no error, check security.access_control first (often a missing IS_AUTHENTICATED_ANONYMOUSLY on login_check), then the user checker. Turn on monolog at DEBUG for security and request channels.
Extension points (the only graceful ones):
UserDiscriminator service definition to change how the type is detected (e.g. from a request attribute rather than session).getRoles() to change role semantics.If you are on Laravel, there is no port and you don't want one. The idiomatic equivalents are multiple guards in config/auth.php + Auth::guard('admin')->attempt(...), a shared parent user model or separate models, and middleware per guard. Copy the architectural idea (provider-per-user-type, separate login routes, per-type user checks) and leave the Symfony bundle behind.
How can I help you explore Laravel packages today?