Entity model
Pattern: MappedSuperclass + ResolveTargetEntityListener
Every entity is split in two:
Base*(bundle) — abstract#[ORM\MappedSuperclass]with all fields, relations, and logic. Lives inDerafu\IdentityBundle\Entity\.- Concrete (app) — extends the Base, adds
#[ORM\Entity]+#[ORM\Table]. Lives inApp\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. Noownercolumn — the owner is the membership withrole.code = 'org.owner'. - OrganizationMembership — user ↔ organization ↔ role. Surrogate
id+ unique constraint(user, organization). FieldjoinedAt. - 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
BaseUserandBaseOrganizationauto-generate UUID v7 and freezecreatedAtat construction time.BaseUserRoleandBaseRolePermissionrequire both FK sides as constructor arguments (they form composite PKs).BaseOrganizationMembershiprequires(user, organization, role).BaseOrganizationInvitationrequires(organization, email, role, tokenHash, expiresAt).BaseVerificationTokenrequires(user, type, expiresAt, tokenHash).
On this page
Last updated on 16/04/2026
by Anonymous