Entity model
Pattern: MappedSuperclass + ResolveTargetEntityListener
Every entity is split in two:
Base*(bundle) — abstract#[ORM\MappedSuperclass]with all fields, relations, and logic. Lives inDerafu\PlatformBundle\{Module}\Entity\.- Concrete (app) — extends the Base, adds
#[ORM\Entity]+#[ORM\Table]. Lives inApp\Entity\{Group}\.
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.
Identity entities
Core identity
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). Implements Symfony’sUserInterfaceandPasswordAuthenticatedUserInterface. - 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).
- RolePermission — which permissions a role grants.
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 with unique
constraint
(user, organization). FieldjoinedAt. - OrganizationInvitation — pending invite. Fields:
email,tokenHash(SHA-256),expiresAt,invitedBy,acceptedAt,acceptedBy,revokedAt,createdAt.
Teams
Organization ──OneToMany──► Team ──OneToMany──► TeamMembership ──ManyToOne──► User
- Team — a named group of org members with a URL-friendly
slug. - TeamMembership — user ↔ team pivot, validates org membership.
Verification tokens
- VerificationToken — one-use token for user lifecycle operations.
Fields:
tokenHash(SHA-256),user,type(enum),payload(JSON),expiresAt,usedAt,createdAt. Types:email_verify,password_reset,email_change,magic_link.
API and JWT tokens
- ApiKey — long-lived programmatic access token. Format:
dik_+ 40 hex chars. Fields:name,tokenHash,displayPrefix,scopes(JSON),lastUsedAt,expiresAt,createdAt. - JwtToken — persisted JWT for audit and revocation. Fields:
jti(JWT ID),user,issuedAt,expiresAt,revokedAt.
Security records
- LoginRecord — one row per successful login: IP, User-Agent,
authenticator,
loginAt. - ImpersonationRecord — one row per
switch_usersession: admin, target, IP,startedAt,endedAt.
Localization
- Locale — supported locale code and display name.
- Timezone — supported timezone identifier and display name.
Apps entities
User / Organization ──OneToMany──► UserAppInstallation / OrganizationAppInstallation
└── config (JSON, encrypted sensitive keys)
- BaseUserAppInstallation — app installed for a specific user.
Fields:
user,appCode,config(JSON),installedAt,active. - BaseOrganizationAppInstallation — app installed for an organization.
Fields:
organization,appCode,config(JSON),installedAt,active. - BaseResourceAppInstallation — app installed for a specific app resource. Extend per resource type with a typed FK to the resource.
All live under Derafu\PlatformBundle\Apps\Entity\.
Notifications entities
User ──OneToMany──► UserWebhookEndpoint ──OneToMany──► UserWebhookDelivery
User ──OneToMany──► UserNotificationPreference
User ──OneToMany──► UserDigestPreference
User ──OneToMany──► InAppNotification
Organization ──OneToMany──► OrganizationWebhookEndpoint
└──OneToMany──► OrganizationWebhookDelivery
- InAppNotification — in-app notification persisted for the user.
Fields:
user,eventCode,data(JSON),readAt,createdAt. - UserWebhookEndpoint / OrganizationWebhookEndpoint — registered
webhook URL. Fields:
url,secret(for HMAC signing),active,eventCodes(JSON array of subscribed events),createdAt. - UserWebhookDelivery / OrganizationWebhookDelivery — delivery
attempt log. Fields:
endpoint,eventCode,payload(JSON),status,statusCode,responseBody,attemptedAt. - UserNotificationPreference — per-user, per-event channel preferences.
- UserDigestPreference — digest schedule preferences.
All live under Derafu\PlatformBundle\Notifications\Entity\.
Extending entities
The concrete entity can add fields without breaking bundle services:
#[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;
}
}
Bundle services, voters, and events only see the entity via its interface
(UserInterface) — extra fields are transparent to the bundle.
Constructors
BaseUserandBaseOrganizationauto-generate UUID v7 and freezecreatedAtat construction time.BaseUserRoleandBaseRolePermissionrequire both FK sides as constructor arguments.BaseOrganizationMembershiprequires(user, organization, role).BaseOrganizationInvitationrequires(organization, email, role, tokenHash, expiresAt).BaseVerificationTokenrequires(user, type, expiresAt, tokenHash).
On this page
Last updated on 28/05/2026
by Anonymous