short-edition/user-bundle
A Symfony bundle for user management that provides a configurable user entity, registration, login, password reset, profile editing, and role-based access control, plus Doctrine integration, customizable templates, and extensible events for building secure user flows in Symfony applications.
Reality check before anything else: despite the user-bundle name, this is a Symfony bundle — a fork of the archived friendsofsymfony/user-bundle (FOSUserBundle). There is no service provider, no config/user-bundle.php, no Eloquent integration, no Auth:: wiring. A Laravel developer's day-to-day leverage on this package inside a Laravel app is zero. You cannot composer require it into a Laravel project and get working auth; the only situations where you touch it are:
Everything below is framed as "what you actually do on day one" in those two situations, mapped to the Laravel mental model you already have.
Minimal steps (legacy Symfony context only):
composer.lock for the exact constraint before changing anything:
composer show short-edition/user-bundle
config/bundles.php (forks frequently rename the bundle class while keeping the upstream FOSUserBundle\... namespace):
ShortEdition\UserBundle\ShortEditionUserBundle::class => ['all' => true],
User entity extends the bundle's mapped superclass and owns its own @ORM\Id / @ORM\Table:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use ShortEdition\UserBundle\Model\User as BaseUser;
/**
* @ORM\Entity
* @ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* @ORM\Id @ORM\Column(type="integer") @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
}
Implementation Patterns — this is where copy-paste from the README breaks).bin/console doctrine:schema:validate first, then doctrine:migrations:diff if you're on a recent Symfony) and smoke-test /register, /login, /resetting/request, /profile.Where to look first (trust code over docs):
DependencyInjection/Configuration.php — the complete config tree; this is the real API surface.Resources/config/routing/ — the subsets you can import (registration.xml, resetting.xml, profile.xml, all.xml).Model/ — User, Group, and the interfaces you're expected to implement.The README is almost always a stale copy of the upstream FOSUserBundle docs. Treat it as a starting hint, never as documentation.
First real use case: a drop-in registration / login / password reset / profile flow — effectively the Symfony analog of laravel/ui or Breeze, except configuration-driven (extend the entity, wire the firewall) rather than scaffolded into your app. That distinction matters: with Breeze you own the generated code and can change anything by editing it; here you own nothing and change things only through extension points (events, form types, config, templates).
If you're actually on Laravel: close this tab and use Breeze, Jetstream, Fortify, or laravel/ui. That is not a dismissal of the package — it's simply the wrong runtime.
Extend, don't edit. The bundle ships Doctrine mapped superclasses (Model/User, Model/Group). You subclass in App\Entity and own the table mapping and ID strategy. This is the Doctrine analog of a base Eloquent model where you override $table / $primaryKey / $fillable — except the base class also carries fields for password, salt, roles, and confirmation tokens that you inherit implicitly, so read it before you add columns.
Firewall-first wiring. The bundle supplies controllers, forms, and listeners; the actual authentication is Symfony's Security component. Nearly every "it redirects in a loop" or "I'm logged out immediately" bug lives in security.yaml, not in the bundle:
# config/packages/security.yaml
security:
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/profile, roles: ROLE_USER }
Ordering of access_control rules matters (first match wins), same as Laravel middleware ordering — but less obvious, because there's no exception telling you a rule was shadowed.
Events are the extension seam, not controller overrides. The Laravel instinct is to extend or replace the controller. Here you subscribe to dispatched events — same shape as Laravel's event/listener pattern, different constants:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use ShortEdition\UserBundle\ShortEditionUserEvents;
class RegistrationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ShortEditionUserEvents::REGISTRATION_CONFIRM => 'onConfirm',
ShortEditionUserEvents::REGISTRATION_COMPLETED => 'onRegistered',
ShortEditionUserEvents::CHANGE_PASSWORD_COMPLETED => 'onPasswordChange',
ShortEditionUserEvents::RESETTING_RESET_COMPLETED => 'onReset',
];
}
}
Confirm the constant names against the fork's *Events class — forks often keep upstream constants but not always. Use bin/console debug:event-dispatcher to see what's actually dispatched.
Customize forms by decoration, not copy-paste. Replace the form type service (e.g. fos_user.registration.form.type) or point config at your own class. Add validation on your extended entity — the equivalent of Form Request rules, but expressed as Doctrine/Validator annotations on the entity.
Templates live in your app. Override via templates/bundles/<BundleName>/... (Symfony 4.1+). This is the same idea as php artisan vendor:publish into resources/views/vendor, except there's no command — it's pure directory convention and silent when you get the path slightly wrong.
Mail is configuration, not code. Registration confirmation and password reset both send mail. Set from_email and a working mailer transport in the bundle config, or the reset token is generated, the request "succeeds," and nothing is delivered. In a Symfony 3/4 app this is swiftmailer; in a newer app it's symfony/mailer.
Import only the route subsets you need. routing/registration.xml + routing/resetting.xml + routing/profile.xml is often preferable to all.xml, which also enables change-password and group routes you may not want exposed.
If you're bridging to Laravel, don't. A service-boundary adapter that translates FOSUserBundle's session/auth events into Laravel guards and syncs the fos_user table into users duplicates persistence, doubles the surface where a password hash can be mishandled, and creates two sources of truth for "is this user logged in." If the product surface is becoming Laravel, the correct pattern is a strangler migration:
users to an Eloquent-compatible table (see the hash gotcha below)./login, then /resetting, then /profile).This is more work than "adopt the bundle" but it's a bounded, one-time cost instead of an indefinite maintenance contract.
The namespace is not the package name. short-edition/user-bundle may register as ShortEdition\UserBundle\ShortEditionUserBundle, SHORTEdition\..., or even retain FOS\UserBundle\FOSUserBundle from upstream. Always confirm from composer show -a + config/bundles.php + the bundle's root class file, not from the README.
Password hash incompatibility is the migration landmine. FOSUserBundle-era code uses Symfony's older security.encoders (bcrypt, or custom MessageDigestPasswordEncoder with a salt). Modern Symfony uses security.password_hashers and modern Laravel uses bcrypt/argon via Hash::. If you migrate users, do not bulk-rehash silently: either use Hash::needsRehash() + rehash-on-login, or force a password reset on every user crossing the boundary. Verify the actual algorithm in the config before assuming bcrypt — some FOSUserBundle installs use a digest-with-salt construction that no Laravel driver reproduces.
anonymous: true is dead in Symfony 6. If you paste the classic firewall block into a modern Symfony app it will fail config validation. This is the most common copy-paste failure and it looks like a bundle problem, so people chase the wrong dependency.
Silent mail failure is the #1 support ticket. from_email unset, transport misconfigured, or a spam-filter rejection all result in "the site says it sent the reset email." Check var/log/, and in dev use a mail catcher. In Symfony 3/4 this is often a Swift Mailer spool table, so also inspect swiftmailer spool queues if configured.
Firewall redirect loops. Symptom: ERR_TOO_MANY_REDIRECTS on /login. Cause is almost always an access_control rule that matches /login and requires auth. Use bin/console debug:router + debug:config security rather than guessing, and remember access_control is first-match.
No stars, no dependents, no score. For an auth package, that is a security signal, not a popularity nitpick. Auth code is the highest-blast-radius code in the app; an unaudited fork means you are the security reviewer. Before adopting, read the diff against upstream FOSUserBundle with focus on:
Model/UserManager, Util/ encoder classes)Util/TokenGenerator, Model/User::$confirmationToken)Controller/RegistrationController)Fork-and-freeze is the sane operating mode. If you must keep the package alive in a legacy app, pin it in composer.json to an exact version, vendor the source, and maintain a small patch set inside your repo. Do not track upstream releases from a fork with no release cadence.
Extension points, ranked by preference: event subscribers → form type service decoration → config flags → template overrides → subclassed entity → (last resort) vendored patch. Overriding a controller is a fork in disguise; avoid it.
Debugging commands that pay for themselves (Symfony side): bin/console debug:router, debug:config security, debug:container fos_user.user_provider.username_email, debug:event-dispatcher. In Laravel you'd reach for route:list, config('auth'), and event fire traces — same debugging shape, different command surface.
Don't run two user providers against the same table. If you attempt a partial Laravel bridge, you'll have FOSUserBundle's provider and a Laravel Eloquent provider reading fos_user — each with its own notion of the current user and its own login session. That is not a working configuration; pick one owner per table.
Out of scope upstream, do not plan around it: social login, MFA, passwordless, API tokens, SPA/stateless auth. If a roadmap item needs those, the bundle is not the enabler — a managed IdP or Sanctum/Passport is.
How can I help you explore Laravel packages today?