pugx/multi-user-bundle
PUGX MultiUserBundle extends FOSUserBundle so Symfony applications can support several distinct user types. It provides separate user entities with shared registration, login, form, controller, and profile handling, simplifying multi-role account management without duplicating authentication code.
pugx/multi-user-bundle is a Symfony bundle (2.x/3.x era) that sits on top of FOSUserBundle and lets several distinct user entities — e.g. Customer and Staff — share one login form while keeping separate registration forms, profiles and tables. It is not a Laravel package; if you're coming from Laravel, mentally map it to "one Auth guard, multiple Eloquent models implementing Authenticatable, each with its own registration controller."
Minimum steps:
composer require pugx/multi-user-bundle — but first confirm your Symfony version. The bundle targets Symfony 2.3–3.x and FOSUserBundle 1.x/2.x. There is no Symfony 4/5/6 support, and the repo is archived, so treat any install as a legacy-maintenance task, not a greenfield one.fos_user config). The multi-user bundle reuses FOSUser's routes, events, controllers and templates; almost every problem you hit is really an FOSUserBundle problem.MappedSuperclass base User plus one concrete entity per type (UserOne, UserTwo), each in its own table via joined inheritance — or fully independent entities each implementing UserInterface. The first option is what the docs assume and what plays nicest with the bundle's user manager.pugx_multi_user.users:pugx_multi_user:
users:
customer:
entity:
class: AppBundle\Entity\Customer
registration:
form:
type: AppBundle\Form\RegistrationCustomerFormType
name: customer_registration
template: AppBundle:Registration:customer.html.twig
staff:
entity:
class: AppBundle\Entity\Staff
registration:
form:
type: AppBundle\Form\RegistrationStaffFormType
name: staff_registration
template: AppBundle:Registration:staff.html.twig
/register), and point your firewall's provider at the multi-user provider instead of the stock FOSUser one./register/customer, submit, confirm the row lands in the customer table, then log in with that account through the single shared login form. If that works, the plumbing is correct and the rest is form/template work.Where to look first in the codebase: DependencyInjection/Configuration.php (the exact config keys), Security/UserProvider.php (how login resolves a user), Form/Type/RegistrationFormType.php (the base class you must extend), and Resources/config/routing/.
Per-type registration form types. Every registration form must extend PUGX\MultiUserBundle\Form\Type\RegistrationFormType and return the config key from getEntityName(). This is the contract the bundle uses to pick the entity and manager:
class RegistrationCustomerFormType extends RegistrationFormType
{
public function getEntityName()
{
return 'customer'; // must match pugx_multi_user.users.customer
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('companyName');
}
}
Shared login, branched persistence. The common production shape is: one login template and one firewall, but N registration entry points linked from a "who are you?" landing page. Authentication is resolved by the bundle's user provider, which loops over the configured entity classes and returns the first match. Keep that loop's ordering in mind (see Gotchas).
Discriminator-driven flows. The bundle keeps a UserDiscriminator service that records "which user type are we currently dealing with" (typically session-backed). Use it — don't reimplement it — when you need a multi-step registration wizard or a type-specific profile edit page: set the type before delegating to the FOSUser controller, and the correct manager/entity is resolved for you.
Per-type profiles and settings. FOSUserBundle only knows about one profile form type. The usual pattern is a thin controller that reads the authenticated user's class, picks the matching form type and template, and renders it — rather than fighting fos_user.profile.form.type. Keep a small userType => [formType, template, route] map in config so the mapping lives in one place.
Cross-cutting behaviour via FOSUser events. Rather than subclassing the bundle's controllers, subscribe to fos_user.user.created, fos_user.security.login etc. and branch on instanceof Customer / instanceof Staff. This is where you assign roles, send type-specific welcome mail, or create a related profile record. It's the cleanest extension point the bundle leaves you.
Service decoration for observability. pugx_multi_user.user_provider and pugx_multi_user.user_manager are ordinary services. Decorating the provider to log which class matched (and which were tried) turns opaque login failures into debuggable events. This is the single highest-value customisation in a multi-tenant-ish setup.
Testing. Write functional tests per registration route (assert row in the right table, assert wrong-table row absent) plus one test that logs in a user of each type through the shared form. Unit-testing the provider in isolation is easy: mock the entity managers and assert resolution order.
User entity with a type discriminator column, or use Symfony's ChainUserProvider (5.3+) with a custom authenticator. Reserve this bundle for maintaining an existing Symfony 2/3 app.admin in two tables means one of them can never log in — and nothing warns you. Either namespace logins per type (a discriminator field in the login form) or enforce globally unique usernames/emails at the application level; the database won't do it for you across separate tables.UNIQUE on the email column only protects one table. Duplicate emails across user types will break password-reset, which looks up a single user by email. Audit this before go-live.getEntityName() mismatch is the classic failure. If the string doesn't exactly match a key under pugx_multi_user.users, you get a vague "user type not found"-style error at submit time, not at container compile time. Double-check spelling and that the form type service is registered with the right tag/alias.@DiscriminatorMap must include every concrete entity, and adding a third user type later means a schema migration plus a config change plus a new form type — three places, easy to half-do. With separate tables you lose polymorphic queries (findAll() across users doesn't exist).provider must be the multi-user provider; leaving FOSUser's default in place makes registration work but login fail with "Bad credentials" for every non-default type. With multiple firewalls you must define a provider per firewall — the bundle's provider is not automatically per-firewall aware.@PUGXMultiUserBundle. Overriding means copying into app/Resources/ and re-importing routes yourself. Budget for this — it's more surgery than a typical bundle.debug:container pugx_multi_user to confirm services and arguments; debug:router to confirm registration routes and their {userType} requirements; inspect the session for the discriminator key; enable Doctrine SQL logging to see which table the provider queried. Nine times out of ten the answer is "wrong entity class resolved" or "wrong firewall provider".pugx_multi_user.user_provider, pugx_multi_user.user_manager, and pugx_multi_user.user_discriminator; extend the base registration form type; subscribe to FOSUser events. Anything else means forking or copying controllers.UserResolver interface from day one. When you migrate to a supported Symfony version, the swap to a chain provider or single-entity-plus-type design becomes a service definition change rather than a rewrite of every controller and template.How can I help you explore Laravel packages today?