MessageThreadRolesTrait
Resolves the `owner` and `receiver` roles over a private thread for the `MessageThread`, `Message` and `MessageParticipant` Api presenters.
A thread or message has no single owner, so the roles cannot be derived from the entity
(OwnerAwareInterface / ReceiverAwareInterface are not implemented here). They are derived
from the authors of the individual messages:
owner: the caller is a thread participant and wrote at least one undeleted message.receiver: the caller is a participant and the thread has an undeleted message they did not write (a system message counts).
A fresh thread is therefore only owner for its creator and only receiver for the other
side; after a reply both have both roles. The permission matrix may grant each role
separately. A non-member (including a guest without an account and an anonymous caller)
never passes and must not learn whether the thread exists.
How it is enforced: actions over a single thread have registrated = 0, owner = 1,
receiver = 1. The gate (ApiPermissionGuardTrait::requireApiPermission()) calls the
presenter's isOwner() / isReceiver(), which delegate to self::isThreadOwner() /
self::isThreadReceiver() and query the DB via self::resolveThreadRoles(). The
thread id arrives only as ownedData['threadId'] (a call parameter) and the identity comes
from the JWT; ownership is never read from the request. These actions have no action*(),
so the external path (RbacCheckTrait) cannot reach them.
Lists without a single record (myThreads, myUnreadCount, MessageParticipant:myList)
stay registrated = 1 with a server-side scope because a conditional role has no record to judge.
Table of Contents
Properties
- $threadRolesCache : array<string, array{member: bool, owner: bool, receiver: bool}>
Methods
- isThreadOwner() : bool
- Tells whether the user owns the thread, i.e. authored at least one undeleted message.
- isThreadReceiver() : bool
- Tells whether the user is a receiver, i.e. the thread has an undeleted message they did not write; false without an identity.
- notMemberWriteShape() : array<string, mixed>
- Returns the response for a non-member on write actions, the same as the manager returns (`USER_UNAUTHORIZED`).
- resolveThreadRoles() : array{member: bool, owner: bool, receiver: bool}
- Returns the caller's roles over the thread by querying participants and messages in the DB.
- threadDenial() : array<string, mixed>
- Translates a gate denial into the response of an action over a thread.
- threadIdFrom() : int
- threadRolesOf() : array{member: bool, owner: bool, receiver: bool}
Properties
$threadRolesCache
private
array<string, array{member: bool, owner: bool, receiver: bool}>
$threadRolesCache
= []
Methods
isThreadOwner()
Tells whether the user owns the thread, i.e. authored at least one undeleted message.
protected
isThreadOwner(int $userId[, array<string|int, mixed> $data = [] ]) : bool
The thread id comes from the call parameter (ownedData) and the entity is not read,
because neither a message nor a thread yields its author without a query.
The presenter wires it into the gate through its own isOwner(): the custom classification
in OwnershipFallbackCoverageTest and in the permission matrix recognizes a custom check
by isOwner()/isReceiver() being declared on the presenter, not on the trait.
Parameters
- $userId : int
- $data : array<string|int, mixed> = []
Return values
boolisThreadReceiver()
Tells whether the user is a receiver, i.e. the thread has an undeleted message they did not write; false without an identity.
protected
isThreadReceiver(int|null $userId[, array<string|int, mixed> $data = [] ]) : bool
Parameters
- $userId : int|null
- $data : array<string|int, mixed> = []
Return values
boolnotMemberWriteShape()
Returns the response for a non-member on write actions, the same as the manager returns (`USER_UNAUTHORIZED`).
protected
static notMemberWriteShape([bool $withCode = true ]) : array<string, mixed>
Parameters
- $withCode : bool = true
Return values
array<string, mixed>resolveThreadRoles()
Returns the caller's roles over the thread by querying participants and messages in the DB.
protected
abstract resolveThreadRoles(int $threadId, int $userId) : array{member: bool, owner: bool, receiver: bool}
Implemented by the presenter through its Api manager.
Parameters
- $threadId : int
- $userId : int
Return values
array{member: bool, owner: bool, receiver: bool}threadDenial()
Translates a gate denial into the response of an action over a thread.
protected
threadDenial(array<string, mixed> $denied, int $threadId, array<string, mixed> $notMemberShape) : array<string, mixed>
Call it only with a non-null result of requireApiPermission(), which the action returns
as its first statement:
if (($denied = $this->requireApiPermission('myThreadReply', ownedData: ['threadId' => $threadId])) !== null) {
return $this->threadDenial($denied, $threadId, self::notMemberWriteShape());
}
A denial of a non-member becomes $notMemberShape, exactly the response the manager's
membership check gives (empty list / success = false), so the response shape never
reveals which thread exists. A member who lacks the role grant gets the regular gate
denial, and so does an anonymous caller.
Parameters
- $denied : array<string, mixed>
- $threadId : int
- $notMemberShape : array<string, mixed>
Return values
array<string, mixed>threadIdFrom()
private
static threadIdFrom(array<string|int, mixed> $data) : int
Parameters
- $data : array<string|int, mixed>
Return values
intthreadRolesOf()
private
threadRolesOf(int $threadId, int $userId) : array{member: bool, owner: bool, receiver: bool}
Parameters
- $threadId : int
- $userId : int