kimai/user-bundle
Symfony bundle that adds user management to Kimai, including user entities, roles, authentication integration, and admin screens for creating, editing, and organizing users within the time-tracking application.
Know what you're installing. kimai/user-bundle is not a general-purpose user library — it is a subtree split of the kimai/kimai monorepo (the src/UserBundle directory), published as a standalone repo for packaging/autoloading reasons. It is archived, has essentially no independent consumer base, and is only meaningful alongside kimai/core-bundle, kimai/timesheet-bundle, and the Kimai application skeleton. Treat it as a component of Kimai, not a dependency you reach for in a new Symfony app.
Minimal setup
In practice you get it via the monorepo:
git clone https://github.com/kimai/kimai.git && cd kimai
composer install
bin/console kimai:install
If you are on a Kimai version that consumed split packages, composer require kimai/user-bundle:^2.0 works — but only inside that app's dependency graph. Standalone installs will immediately collide on Doctrine entity mappings, symfony/security-bundle version, and the Kimai\CoreBundle\ classes it type-hints against.
The bundle registers itself as Kimai\UserBundle\KimaiUserBundle in config/bundles.php. Services, routes, and entity mappings are loaded from Resources/config/.
Configuration lives under the kimai: key of config/packages/kimai.yaml, with the canonical schema in src/DependencyInjection/Configuration.php. Read that file before guessing at keys — the public docs lag the code.
Where to look first
src/Entity/User.php, Team.php, Role.php — the domain model. User is your UserInterface, holds roles, locale/timezone, enabled flag, and a collection of UserPreference.src/Repository/UserRepository.php — also the UserLoaderInterface backing the Symfony security provider.src/Events/ — the real extension surface (UserCreateEvent, UserUpdateEvent, team events).src/Security/ — voter/permission wiring; this is where "can this user see that timesheet" is decided.src/Controller/ — UserController, TeamController, RoleController, RegistrationController, ProfileController. These show you the intended admin UX.First use case: provision an admin and a teamlead, then confirm scoping.
bin/console kimai:user:create # interactive; add --help for non-interactive flags
bin/console kimai:user:promote <username> ROLE_TEAMLEAD
Then log in as the teamlead, create a team in /en/team/, add a member, and verify the timesheet listing only exposes data for that team's customers/projects. That single loop exercises the entity model, the permission voter, and the team scoping you'll be debugging constantly.
CLI-first provisioning. For production, script user lifecycle rather than clicking. kimai:user:create, kimai:user:promote, and kimai:user:demote are the stable surface; wrap them in deploy/onboarding scripts and treat the admin UI as a fallback. This survives UI refactors and gives you an auditable path.
Event subscribers are the intended extension point. Because the entity is Doctrine-mapped inside a bundle you don't own, the supported way to react to user lifecycle is the event dispatcher.
namespace App\EventListener;
use Kimai\UserBundle\Event\UserCreateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class ProvisionUserSubscriber implements EventSubscriberInterface
{
public function __construct(private CrmClient $crm) {}
public static function getSubscribedEvents(): array
{
return [UserCreateEvent::class => 'onCreate'];
}
public function onCreate(UserCreateEvent $event): void
{
$user = $event->getUser();
$this->crm->createContact($user->getEmail(), $user->getUsername());
// seed defaults, e.g. preferences / language / timezone
}
}
Tag it kernel.event_subscriber (autoconfigured in Kimai's service config). The same pattern applies to update events for syncing display names, disabled flags, or external identity mappings. This is exactly how the LDAP/SAML-style identity plugins hook in — read their source for the reference implementation.
Teams are the authorization unit, roles are the coarse switch. Kimai has effectively two axes:
ROLE_USER, ROLE_TEAMLEAD, ROLE_ADMIN, ROLE_SUPER_ADMIN) set the ceiling.Model your organisation as one team per customer (or per department), put the customer-facing consultant in the relevant team(s), and give one person per team ROLE_TEAMLEAD. Do not try to encode scoping in roles; you'll fight the voter.
Programmatic creation when you need more than the command offers:
$user = new User();
$user->setUsername('jane');
$user->setEmail('[email protected]');
$user->setPlainPassword($generated); // hashed by the bundle's listener/service
$user->setEnabled(true);
$user->setRoles(['ROLE_USER']);
$user->setPreference('language', 'de'); // per-user preference key/value
// timezone: leave null to fall back to the app default
$em->persist($user);
$em->flush();
If you bypass whatever service orchestrates creation, remember to dispatch the create event yourself — otherwise your CRM subscriber silently doesn't fire.
Registration and password reset are controlled through the kimai: config tree (registration on/off, mailer behaviour) plus the RegistrationController routes. If you need a bespoke signup (SSO pre-provisioning, invitation tokens), override the controller route in your app's routing rather than patching the bundle.
Permission checks in your own controllers/plugins go through the standard voter:
if (!$this->isGranted('view_other_timesheet')) { throw new AccessDeniedException(); }
Never assume isGranted('ROLE_ADMIN') implies access to a specific record — the voter combines role ceiling with team scope, and ROLE_SUPER_ADMIN is the one bypass.
Preferences over schema changes. When you need "extra field on the user", first try a UserPreference key. It's indexed, serialised through the profile form's extension hooks in plugins, and avoids the migration problem described below.
kimai/kimai; this repo is a read-only mirror. File issues upstream, and pin the app version rather than the split package version — the two drift.User entity by inheritance. Doctrine single-table inheritance across a vendor bundle mapping is a migration nightmare and the bundle's forms, validators, and repository queries assume the concrete class. Use events, preferences, or a side table with a one-to-one relation you own.UniqueConstraintViolationException at flush time. If you build a custom creation controller, validate with the bundle's constraints (or a UniqueEntity) before persisting, or you'll hand users a 500.setEnabled(false). Hard deletes will either be blocked by relations or orphan reporting. Disable, don't delete, and let a retention policy handle the rest.MAILER_DSN. The default (sendmail) is frequently absent in containers — set a real DSN and test the reset flow explicitly, because the failure surfaces as "no email arrived", not as an exception.kimai: config and container; bin/console cache:clear after editing kimai.yaml or you'll debug phantom behaviour for twenty minutes.DependencyInjection/Configuration.php for every config key. The documentation is thin for this bundle, and several options changed shape between 1.x and 2.x. The schema file is the only reliable contract.tests/; use them in integration tests for anything touching scoping, because the permission matrix (role × team × ownership) has more edge cases than you'll enumerate by hand.How can I help you explore Laravel packages today?