Entity model

Pattern: MappedSuperclass + ResolveTargetEntityListener

Every entity is split in two:

  • Base* (bundle) — abstract #[ORM\MappedSuperclass] with all fields, relations, and logic. Lives in Derafu\IdentityBundle\Entity\.
  • Concrete (app) — extends the Base, adds #[ORM\Entity] + #[ORM\Table]. Lives in App\Entity\Auth\.

ORM relations in the bundle target interfaces (not concrete classes). Doctrine resolves them at runtime via resolve_target_entities, wired automatically by the bundle’s DI extension.

Entity map

Identity core

User ──OneToMany──► UserRole ◄──ManyToOne── Role
                                              │
                                    OneToMany─┘
                                              │
                                              ▼
                                       RolePermission
                                              │
                                    ManyToOne─┘
                                              ▼
                                         Permission
  • User — the authenticated subject. Fields: id, uuid (v7), name, email (unique, login identifier), password (hashed), active, emailVerifiedAt, createdAt, config (JSON).
  • Role — reusable role identified by a unique code (e.g. org.admin). No scope column — the scope is defined by the relation that uses the role.
  • Permission — atomic capability identified by a unique code (e.g. invoice.create).
  • UserRole — global system-level role assignment (super-admin, auditor). Composite PK (user, role).
  • RolePermission — which permissions a role grants. Composite PK (role, permission).

Organizations

User ──OneToMany──► OrganizationMembership ◄──OneToMany── Organization
                         │                                     │
                    ManyToOne → Role                    OneToMany
                                                               │
                                                               ▼
                                                 OrganizationInvitation
  • Organization — a tenant. Fields: id, uuid, name, active, createdAt, config. No owner column — the owner is the membership with role.code = 'org.owner'.
  • OrganizationMembership — user ↔ organization ↔ role. Surrogate id + unique constraint (user, organization). Field joinedAt.
  • OrganizationInvitation — pending invite. Fields: email, tokenHash (SHA-256), expiresAt, invitedBy, acceptedAt, acceptedBy, revokedAt, createdAt. Lifecycle: isActive() = not accepted, not revoked, not expired.

Verification tokens

  • VerificationToken — one-use token for user lifecycle operations. Fields: tokenHash (SHA-256), user, type (enum: email_verify, password_reset, email_change), payload (JSON), expiresAt, usedAt, createdAt.

Extending entities

The concrete entity can add fields:

#[ORM\Entity]
#[ORM\Table(name: 'auth_users')]
class User extends BaseUser
{
    #[ORM\Column(type: Types::STRING, length: 20, nullable: true)]
    private ?string $phone = null;

    public function getPhone(): ?string
    {
        return $this->phone;
    }

    public function setPhone(?string $phone): static
    {
        $this->phone = $phone;

        return $this;
    }
}

The bundle’s services, voters, and events will continue working — they only see the UserInterface.

Constructors

  • BaseUser and BaseOrganization auto-generate UUID v7 and freeze createdAt at construction time.
  • BaseUserRole and BaseRolePermission require both FK sides as constructor arguments (they form composite PKs).
  • BaseOrganizationMembership requires (user, organization, role).
  • BaseOrganizationInvitation requires (organization, email, role, tokenHash, expiresAt).
  • BaseVerificationToken requires (user, type, expiresAt, tokenHash).
On this page

Last updated on 16/04/2026 by Anonymous