monofony/user-bundle
Monofony UserBundle integrates user management into Monofony applications: customer and admin user entities, authentication, registration, password reset and profile handling. It builds on Sylius resources and Symfony security to give you a ready-to-extend user system out of the box.
This is a Symfony bundle, not a Laravel package. It is a read-only subtree split of the UserBundle from the Monofony monorepo, which is itself a rename of Sylius/SyliusUserBundle. If you're coming from Laravel, understand up front: there is no Auth::guard(), no Authenticatable contract, no Hash::make(). Day-to-day work here means writing Doctrine entities, YAML security config, and DI service definitions. You cannot composer require this into a Laravel app without dragging in Symfony Security, the DI container, and Doctrine ORM.
Minimal install (v1.10.0 dropped PHP 7.3 and added PHP 8 — check the constraint before pinning; on 7.3 you are stuck on the previous tag):
composer require monofony/user-bundle
# then read composer.json: does it require sylius/* or monofony/* components, and what are the php/ext constraints?
Register the bundle (find the real class name in the package's composer.json autoload section — it is the Sylius bundle with the namespace swapped):
// config/bundles.php
return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true],
Monofony\Bundle\UserBundle\MonofonyUserBundle::class => ['all' => true],
];
Create concrete users by extending the abstract base model shipped by the component fork:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Monofony\Component\User\Model\User as BaseUser;
/**
* @ORM\Entity
* @ORM\Table(name="app_admin_user")
*/
class AdminUser extends BaseUser
{
}
Wire two firewalls/providers in security.yaml (one per user type):
security:
providers:
admin: { entity: { class: App\Entity\AdminUser, property: username } }
shop: { entity: { class: App\Entity\ShopUser, property: email } }
password_hashers: # security.encoders on Symfony < 5.3
App\Entity\AdminUser: bcrypt
App\Entity\ShopUser: bcrypt
firewalls:
admin:
pattern: ^/admin
provider: admin
form_login: { login_path: admin_login, check_path: admin_login_check }
logout: { path: admin_logout }
shop:
pattern: ^/
provider: shop
form_login: { login_path: shop_login, check_path: shop_login_check }
Where to look first, in this order:
DependencyInjection/Configuration.php — the canonical list of every config key and resource definition.Resources/config/doctrine/model/User.orm.xml — tells you which fields are mapped as MappedSuperclass and which you must map yourself.Resources/config/services.xml — the service IDs you will end up decorating (user_manager, password updater, canonicalizers, token generator).First use case: an admin user logging in to /admin via form login, and a shop customer registering and logging in on the storefront, each with a separate provider and separate table. That exercise touches the model, the provider, the password lifecycle, and the security config — the four things you'll customize in every project.
The "multiple users" feature is just multiple concrete classes. There is nothing magic: extend the base User once per persona, give each its own table, repository, and firewall. The base model lives in the component package (Monofony\Component\User\Model\User), not the bundle, so your entities extend that and you register each with SyliusResourceBundle:
sylius_resource:
resources:
app.admin_user:
driver: doctrine/orm
classes:
model: App\Entity\AdminUser
repository: App\Repository\AdminUserRepository
form: App\Form\AdminUserType
app.shop_user:
driver: doctrine/orm
classes:
model: App\Entity\ShopUser
repository: App\Repository\ShopUserRepository
Always create and update users through the manager, never via EntityManager::persist() directly. The manager is what runs the canonicalizers (lowercasing usernameCanonical/emailCanonical), dispatches the password-update step that hashes plainPassword into password, clears the plaintext, and generates the confirmation token. Bypassing it leaves canonical columns null, and your user silently cannot log in.
Password lifecycle is a two-step contract: you set plainPassword on the entity, then persist via the manager. A listener picks up the plaintext, calls the password encoder, and nulls the plaintext. If you're writing tests or fixtures, replicate that by calling the manager — do not hand-set password.
Registration flow is the SyliusResourceBundle pattern, not a custom controller: a resource route with create/store actions, a form type registered in the classes.form slot, and a redirect on success. Customize the form type; don't fork the controller.
Password reset uses the token generator + TTL services. The bundle gives you the token primitives and the token-TTL config; you supply the mailer and the reset form. Hook this up as your own controller that calls the manager's token methods, then a second action that consumes the token and reassigns plainPassword.
OAuth is thin scaffolding, not a solution. Expect model fields like oauthProvider/oauthIdentifier, a repository finder for them, and a form type. The actual OAuth dance is HWIOAuthBundle (or knpuniversity/oauth2-client-bundle) with its own firewall, resource owners, and an oauth_user_provider service you write. The pattern that works:
loadUserByOAuthUserResponse, look up by (provider, identifier).Step 3 is where account-takeover bugs live — never auto-link by email unless the provider asserts the email is verified.
Roles. The base model implements Symfony's UserInterface::getRoles(). Whether the roles property is JSON-mapped by the bundle's XML mapping varies between versions — inspect Resources/config/doctrine/model/ before adding a roles column to your entity, or you'll get a duplicate-mapping error. This bundle predates Symfony's Role value objects, so it uses plain string roles like ROLE_ADMIN.
Extension points in order of preference:
classes.model / classes.repository / classes.form in the resource config (supported, upgrade-safe).*.model.user.class) in config/services.yaml — the Sylius-family convention.templates/bundles/<BundleName>/.v1.10.0 is a narrow, dependency-level release: PHP 8 allowed, PHP 7.3 dropped. Nothing else changed. No new features, no deprecation removals, no Symfony 6 work, no bug fixes called out. Read it for what it tells you rather than what it ships: it proves the package still builds and still accepts a modern PHP runtime, and it proves the maintenance is a compatibility bump roughly once every few years (previous tag 2021). Pin the exact version, commit your composer.lock, and budget for forking it into your own src/ if you build anything serious. For a new project, the honest recommendation remains Sylius/SyliusUserBundle (actively maintained, same API surface) or Symfony's own make:user — this package's value is as a reference implementation of the Sylius user model, not as a long-lived dependency.
PHP 8 support is not Symfony 6 support. The release note changes the runtime, not the framework integration. Everything below about encoders/password_hashers, getSalt(), and \Serializable still applies unless you verify it in the source for v1.10.0 — a PHP 8 compatibility commit frequently means "it parses and the test suite is green," not "the deprecations are gone."
Do not install it alongside sylius/sylius-user-bundle. Service IDs are namespaced per bundle so the container may survive, but you'll get two sets of Doctrine mappings for near-identical abstract models, duplicate security listeners, and ambiguous autowiring. Pick one.
Config-key guessing wastes an hour. The key is the Sylius one renamed (sylius_user → monofony_user, or whatever the extension alias actually is). Confirm with:
bin/console debug:config <alias>
bin/console debug:container UserManager --show-private
bin/console doctrine:mapping:info # did your MappedSuperclass actually load?
bin/console debug:firewall admin # are the provider and authenticator what you think?
bin/console lint:container
doctrine:mapping:info is the one you'll run most — "the class is not a valid entity or mapped superclass" almost always means the bundle's XML mapping isn't being auto-loaded (missing auto_mapping, or a custom mapping config that doesn't include the bundle's Resources/config/doctrine).
Symfony 5.3+ rename. security.encoders became security.password_hashers, and getSalt() was removed from the interface. If the model still implements the old contract, expect deprecation noise at minimum and a fatal on getSalt() at worst. Check the shipped model before you write config against it.
Serializable deprecation. The user model implements \Serializable, which Symfony 5.4+ warns about and Symfony 6 deprecates. The user object is serialized into the session; if you change fields on your concrete class, old sessions will throw on deserialization. Either implement __serialize/__unserialize in your entity or plan session invalidation on deploy.
Double-hashing. The most common bug: setting password manually and letting the listener run, or calling the password updater twice in a registration controller. Symptom: login fails with a correct password, and password in the DB looks like a hash of a hash. Test by dumping the plaintext right before persist.
Canonical fields are the second most common bug. usernameCanonical/emailCanonical are what the provider actually queries. If you import users with raw SQL or Doctrine fixtures, populate them (lowercase) or the provider finds nothing.
OAuth expectations. The description says "OAuth support"; the code says "here are two nullable columns and a finder." Don't scope OAuth as a day of work based on the README. Also note HWIOAuthBundle itself is barely maintained — knpuniversity/oauth2-client-bundle plus a custom authenticator is the more durable path today.
Confirmation tokens expire. There's a TTL config; check it before you assume a "broken" reset link is a bug in your controller. Tokens are one-shot — consuming one must clear it.
Debugging a login that "just doesn't work" in order: (1) debug:firewall to confirm the provider; (2) query the DB for usernameCanonical/emailCanonical/enabled; (3) check whether enabled is false — a fresh entity defaults to disabled in the Sylius model, and a disabled user fails authentication with no useful log line; (4) check the password hash prefix matches the configured hasher.
Treat it as vendored source. One compatibility release after a multi-year gap, a single star, and no dependents is not a revival — it is the minimum required to keep the subtree split installable. The realistic day-to-day posture is still: read the code, copy the model and the manager into your app, and own it. That costs an afternoon and removes the entire class of "abandoned dependency" risk, which remains the dominant consideration here.
How can I help you explore Laravel packages today?