nucleos/profile-bundle
Symfony bundle that adds a ready-made profile area for NucleosUserBundle-based apps, letting authenticated users view and edit account details and change passwords through prebuilt controllers, forms, and routes.
Install and enable
composer require nucleos/profile-bundle
Requires Symfony 6.4/7.x, and as of 2.9.0 also Symfony 8 (see PR #576) — matching your app's Symfony major is the only real install constraint. If you don't use Symfony Flex, add the bundle manually:
// config/bundles.php
Nucleos\Bundle\ProfileBundle\NucleosProfileBundle::class => ['all' => true],
Prerequisite: a User entity
The bundle is persistence-agnostic and opinionated only about the Symfony\Component\Security\Core\User\UserInterface contract. You need your own Doctrine entity (or ODM document) implementing UserInterface and PasswordAuthenticatedUserInterface. Typical minimal shape:
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(unique: true)]
private string $username;
#[ORM\Column(unique: true)]
private string $email;
#[ORM\Column]
private string $password;
private ?string $plainPassword = null;
// getters/setters, getRoles(), eraseCredentials()...
}
Wire routes
# config/routes/nucleos_profile.yaml
nucleos_profile:
resource: "@NucleosProfileBundle/config/routes.php"
prefix: /
Then bin/console debug:router should show nucleos_profile_registration_register, nucleos_profile_registration_confirm, nucleos_profile_profile_edit, and the change-password / reset-password routes.
Build your form types
The bundle ships abstract form types whose data_class you must supply, plus the extra fields (password, plainPassword) you need:
// src/Form/RegistrationFormType.php
class RegistrationFormType extends AbstractRegistrationFormType
{
public function __construct() { parent::__construct(User::class); }
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
$builder->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'mapped' => false,
'constraints' => [new NotBlank(), new Length(['min' => 8])],
]);
}
}
// src/Form/ProfileFormType.php
class ProfileFormType extends AbstractProfileFormType
{
public function __construct() { parent::__construct(User::class); }
}
Point the config at them
# config/packages/nucleos_profile.yaml
nucleos_profile:
registration:
form:
type: App\Form\RegistrationFormType
name: app_registration
confirmation:
enabled: true
profile:
form:
type: App\Form\ProfileFormType
name: app_profile
firewall_name: main
from_email:
address: [email protected]
sender_name: My App
First use case: hit /register, submit, confirm the account (if confirmation is on), then /profile/edit. From a developer's perspective, the value is that you get working, CSRF-protected, event-dispatched controllers for the three pieces of user lifecycle that everyone rewrites badly: registration, profile edit, and password change.
The bundle does not hash passwords for you — it just gives you a form field. The canonical pattern is a Doctrine prePersist/preUpdate listener:
final class UserPasswordListener
{
public function __construct(private UserPasswordHasherInterface $hasher) {}
public function prePersist(LifecycleEventArgs $args): void
{
$user = $args->getObject();
if ($user instanceof User && $user->getPlainPassword()) {
$user->setPassword($this->hasher->hashPassword($user, $user->getPlainPassword()));
$user->eraseCredentials();
}
}
public function preUpdate(LifecycleEventArgs $args): void { /* same, with recomputeChangeSet */ }
}
Register it as a doctrine.event_listener (['prePersist', 'preUpdate']). This keeps hashing out of controllers and stays out of the way of admin tools.
The bundle dispatches Nucleos\Bundle\ProfileBundle\Event\UserEvent at well-known constants: REGISTRATION_INITIALIZE, REGISTRATION_SUCCESS, REGISTRATION_COMPLETED, REGISTRATION_CONFIRM, REGISTRATION_CONFIRMED, PROFILE_EDIT_INITIALIZE/SUCCESS/COMPLETED, CHANGE_PASSWORD_*. Subscribe in getSubscribedEvents():
public static function getSubscribedEvents(): array
{
return [
ProfileEvents::REGISTRATION_COMPLETED => 'onRegistrationCompleted',
];
}
public function onRegistrationCompleted(UserEvent $event): void
{
$this->mailer->sendWelcome($event->getUser());
}
This is the right integration seam — don't override the controller just to send an email.
Each shipped controller is a normal service and can be extended or replaced. If you need behavior the events don't cover, subclass RegistrationController (its service ID is nucleos_profile.controller.registration), override one action, and re-point the route resource at your class. Prefer events; subclass only when the event payload is insufficient.
Override by copying the bundle template into templates/bundles/NucleosProfileBundle/Registration/register.html.twig — Symfony's bundle override path is picked up automatically, no config needed. Keep the same block names (content, form) so the controllers' render() calls keep working.
If symfony/security-bundle is installed, set firewall_name in config so that the controller can dispatch the "authenticate after registration" behavior. If you skip security (e.g. API-only use), remove nucleos_profile.firewall_name; the bundle degrades gracefully and won't attempt to log the user in.
Enable registration.confirmation.enabled: true and make sure an email is dispatched from REGISTRATION_CONFIRM (or REGISTRATION_COMPLETED, depending on when you want the link sent). The bundle holds the confirmation token and route; you supply the mailer. For SPAs, override the confirmation template with an HTML page that deep-links back into your frontend rather than relying on the default Twig view.
EntityManager::flush() or a UserManager you write yourself). The controllers dispatch events and render; they don't own the entity manager lifecycle. This is the #1 source of "nothing happens after submit" bugs.plainPassword is not mapped. You must set 'mapped' => false on it in your form and copy it to setPassword() in a listener; otherwise validation will fail or the password is silently dropped.Length(['min' => 8]) you'll accept a as a password.bin/console debug:router and debug:container nucleos_profile.controller.registration to discover the actual service IDs before subclassing — they've shifted between major versions.registration.form.type and registration.form.name are different from what you might guess from FOSUserBundle's flatter layout; keep the YAML indentation right or Symfony will silently ignore the setting.firewall_name mismatch causes silent no-login. If your firewall is admin but config says main, registration succeeds but the session isn't populated. Check with security.firewalls in config/packages/security.yaml.templates/bundles/NucleosProfileBundle/... — an extra d or a lowercase profile will silently fall back to the vendor template.from_email is required for the email listeners to work. If you never configured it and never enabled confirmation, that's fine; if you enable confirmation, the bundle will throw on send because address is null.vendor/nucleos/profile-bundle/src/Controller/ and src/Event/ are small enough to read in full.AbstractTypeExtension, (4) subclassing controllers. Reaching level (4) is almost always a sign your use case belongs in your own controller, not in this bundle.composer update and nothing more. Still pin the major explicitly (^2.9) in composer.json, since this bundle has done breaking config renames between majors before. If you're on Symfony 8, require ^2.9 — earlier 2.x tags cap out on Symfony 7.How can I help you explore Laravel packages today?