CoolCMS API

UserRolePrivilegeMatrixManager
in package

Set-based management of the RBAC permission matrix (rows = privileges, columns = roles, cells = the `allowed` link) directly through DBAL, like {@see \App\UI\Admin\System\Models\Managers\PrivilegeManager}. Doctrine ORM is inconsistent for these tables (derived-identity join plus two parallel entities). Writes are fail-safe: an error is logged and a failure is returned.

It also serves PrivilegeGroupPresenter (group multiplier) and the flat overview of UserRolePrivilegePresenter with WHERE/ORDER/LIMIT/COUNT in SQL.

IMPORTANT: relations are written by upserting single cells (privilege_id, role_id), never by "delete all for the parent, then insert a batch". The group form sends a checkbox for every role, so the cell-wise upsert is complete and never touches a relation of another role or row.

Tables:

  • cms_system_user_role_privileges (privileges: user_role_privilege_id, key, branch, sort, group_id)
  • cms_system_user_role_privilege_relations (links: role x privilege => allowed)
  • cms_system_privilege_groups (joined only for the group label in the flat overview)
  • cms_system_user_role_privilege_texts (privilege name per language; read-only here for display, namesForPrivileges(); names are not edited in this class)

Table of Contents

Constants

GROUP_FK  : mixed = 'cms_system_privilege_groups_privilege_group_id'
GROUP_TABLE  : mixed = 'cms_system_privilege_groups'
KEY_PATTERN  : mixed = '/^:(Admin|Front|Api):[^:\s]+:[^:\s]+:[^:\s]+$/'
Privilege key shape: `:<Admin|Front|Api>:<Module>:<Presenter>:<action>` (a compound action may carry further colon segments).
PRIVILEGE_TABLE  : mixed = 'cms_system_user_role_privileges'
REL_PRIVILEGE  : mixed = 'cms_system_user_role_privileges_user_role_priv...
REL_ROLE  : mixed = 'cms_system_user_roles_user_role_id'
RELATION_TABLE  : mixed = 'cms_system_user_role_privilege_relations'
TEXT_LANG_FK  : mixed = 'cms_system_languages_language_id'
TEXT_TABLE  : mixed = 'cms_system_user_role_privilege_texts'

Properties

$conditionalRoleSupport  : ConditionalRoleSupport
$connection  : Connection
$languageManager  : LanguageManager

Methods

__construct()  : mixed
deletePrivilege()  : Result
Deletes the privilege and its relations (returns a {@see Result}, see {@see setRelation()}).
findOne()  : array{id: int, key: string, branch: string, groupId: ?int, groupKey: ?string}|null
Returns one privilege (edit screen of the flat overview, display plus group reassignment).
findPrivileges()  : array{items: list, total: ?int}
SQL-side filtering, sorting and paging of the flat overview; replaces loading everything and filtering in PHP in `UserRolePrivilegeGridService`.
getGroupRows()  : array<int, array{id: int, key: string, branch: string, sort: int, name: string, relations: array}>
Returns the multiplier rows of the group form: the privileges belonging to the given group (or an empty array for a new or unsaved group), with relations to all roles.
getMatrix()  : array<string, bool>
Returns the matrix state: allowed links keyed "privilegeId-roleId" => true.
getPrivileges()  : array<int, array{id: int, key: string}>
Lists the privileges (matrix rows), unchanged; `UserRolePrivilegeManager` (Admin) uses it to prefill the old multiselect edit form.
groupSaveWire()  : array{ok: bool, error: ?string, ids: list}
Builds the wire shape `{ok, error, ids}` of the `save-group-rows` response. The contract (Admin bridge, external clients) is unchanged by the {@see Result} return type ({@see saveGroupPrivileges()}).
saveGroupPrivileges()  : Result
Saves the group multiplier: inserts or updates rows, upserts the role relations and deletes the removed rows (`deleteIds`), all in one transaction (everything or nothing).
savePrivilege()  : int
Inserts a new privilege or renames an existing one. Returns its id (0 on error).
setPrivilegeGroup()  : Result
Moves the privilege to another group (or to "unassigned" for `null`) through the picker in the flat `UserRolePrivilegePresenter`. Returns a {@see Result} (see {@see setRelation()}).
setRelation()  : Result
Sets (upserts) one cell of the role x privilege matrix.
branchFromKey()  : string
First key segment (`:Api:...` gives `Api`); an invalid shape gives `Admin` (fail-safe default; the write validates beforehand anyway).
buildFlatWhere()  : ParameterType>}
Filters: `branch` (string), `groupId` (int, or `false` for "unassigned"/`IS NULL`; `null` or missing means no group filter), `key` (string, `LIKE`).
findUnsupportedConditionalGrant()  : string|null
A new grant of a conditional role on a key whose presenter cannot evaluate that role would be dead (fail-closed deny) and confusing, so the server rejects it even for a hand-forged POST.
isExistingGrant()  : bool
Is this grant (row with the same key x role, `allowed = 1`) already stored in the database?
namesForPrivileges()  : array<int, string>
Returns the privilege names in the current admin language with one query for a list of ids.
relationsForPrivileges()  : array<int, array<int, bool>>
Returns the relations (role => allowed) for a list of privileges with one query.
toInt()  : int
toStr()  : string

Constants

KEY_PATTERN

Privilege key shape: `:<Admin|Front|Api>:<Module>:<Presenter>:<action>` (a compound action may carry further colon segments).

private mixed KEY_PATTERN = '/^:(Admin|Front|Api):[^:\s]+:[^:\s]+:[^:\s]+$/'

Properties

Methods

deletePrivilege()

Deletes the privilege and its relations (returns a {@see Result}, see {@see setRelation()}).

public deletePrivilege(int $id) : Result
Parameters
$id : int
Return values
Result

findOne()

Returns one privilege (edit screen of the flat overview, display plus group reassignment).

public findOne(int $id) : array{id: int, key: string, branch: string, groupId: ?int, groupKey: ?string}|null
Parameters
$id : int
Return values
array{id: int, key: string, branch: string, groupId: ?int, groupKey: ?string}|null

findPrivileges()

SQL-side filtering, sorting and paging of the flat overview; replaces loading everything and filtering in PHP in `UserRolePrivilegeGridService`.

public findPrivileges(array<string, mixed> $filters[, string $orderBy = 'key' ][, string $orderDir = 'ASC' ][, int $offset = 0 ][, int $limit = 50 ][, bool $computeTotalCount = false ][, int|null $langId = null ]) : array{items: list, total: ?int}

Filters: branch (string), groupId (int, or false for "unassigned"/IS NULL; null or missing means no group filter), key (string, LIKE over the key and the group).

$langId is the current admin language (the grid is a read-only overview, the name is not edited); names are looked up through namesForPrivileges() only for the returned page (at most $limit rows), not for the whole table; falls back to the default application language, and to an empty string when that is missing too.

Parameters
$filters : array<string, mixed>
$orderBy : string = 'key'
$orderDir : string = 'ASC'
$offset : int = 0
$limit : int = 50
$computeTotalCount : bool = false
$langId : int|null = null
Return values
array{items: list, total: ?int}

getGroupRows()

Returns the multiplier rows of the group form: the privileges belonging to the given group (or an empty array for a new or unsaved group), with relations to all roles.

public getGroupRows(int|null $groupId[, int|null $langId = null ]) : array<int, array{id: int, key: string, branch: string, sort: int, name: string, relations: array}>

$langId is the current admin language (the matrix in PrivilegeGroupFormTrait only displays the name read-only; it is looked up here and attached to the row). null or a missing translation falls back to the default language (namesForPrivileges()).

Parameters
$groupId : int|null
$langId : int|null = null
Return values
array<int, array{id: int, key: string, branch: string, sort: int, name: string, relations: array}>

getMatrix()

Returns the matrix state: allowed links keyed "privilegeId-roleId" => true.

public getMatrix() : array<string, bool>

NOTE: loads the whole matrix (about 6000 rows); kept only for backward compatibility with the old call. The newer screens (group grid, multiplier, flat overview) use getGroupRows() / findPrivileges(), which read a limited range.

Return values
array<string, bool>

getPrivileges()

Lists the privileges (matrix rows), unchanged; `UserRolePrivilegeManager` (Admin) uses it to prefill the old multiselect edit form.

public getPrivileges() : array<int, array{id: int, key: string}>
Return values
array<int, array{id: int, key: string}>

groupSaveWire()

Builds the wire shape `{ok, error, ids}` of the `save-group-rows` response. The contract (Admin bridge, external clients) is unchanged by the {@see Result} return type ({@see saveGroupPrivileges()}).

public static groupSaveWire(Result $result) : array{ok: bool, error: ?string, ids: list}
Parameters
$result : Result
Return values
array{ok: bool, error: ?string, ids: list}

saveGroupPrivileges()

Saves the group multiplier: inserts or updates rows, upserts the role relations and deletes the removed rows (`deleteIds`), all in one transaction (everything or nothing).

public saveGroupPrivileges(int|null $groupId, array<int, array{id: ?int, key: string, sort: int, roles: array}> $rows, array<int, int> $deleteIds[, int|null $ownerRoleId = null ]) : Result

IMPORTANT: branch is not taken from the form; it is derived from the first segment of the key (the single source of truth). A key that does not match the :Admin|Front|Api:... shape rejects the whole save (no partial write): validation runs before any database write.

IMPORTANT: a role with conditional=1 (owner, receiver) is never stored as allowed=1 for rows outside branch=Api, even if the form sends it. It is enforced here, not only in the UI, as defense in depth against a bypassed disabled checkbox on the client.

IMPORTANT: a new grant of a conditional role on an :Api key whose presenter cannot evaluate it is rejected (CONDITIONAL_ROLE_NOT_SUPPORTED:<role>:<key>, findUnsupportedConditionalGrant()); already stored dead grants are left alone.

Returns a Result (always truthy; check ->isSuccess()). Success carries data = ['ids' => list<int>] (the saved ids); failure carries a machine token in message (INVALID_KEY_FORMAT:<key>, DUPLICATE_KEY_IN_BATCH, CONDITIONAL_ROLE_NOT_SUPPORTED:<role>:<key>, DUPLICATE_KEY, DB_ERROR). The wire shape {ok, error, ids} for the Admin and external clients is built by the Api presenter (groupSaveWire()).

Parameters
$groupId : int|null
$rows : array<int, array{id: ?int, key: string, sort: int, roles: array}>
$deleteIds : array<int, int>
$ownerRoleId : int|null = null
Return values
Result

savePrivilege()

Inserts a new privilege or renames an existing one. Returns its id (0 on error).

public savePrivilege(int|null $id, string $key) : int
Parameters
$id : int|null
$key : string
Return values
int

setPrivilegeGroup()

Moves the privilege to another group (or to "unassigned" for `null`) through the picker in the flat `UserRolePrivilegePresenter`. Returns a {@see Result} (see {@see setRelation()}).

public setPrivilegeGroup(int $id, int|null $groupId) : Result
Parameters
$id : int
$groupId : int|null
Return values
Result

setRelation()

Sets (upserts) one cell of the role x privilege matrix.

public setRelation(int $roleId, int $privilegeId, bool $allowed) : Result

IMPORTANT: returns a Result, which is always truthy; the caller must check ->isSuccess().

Parameters
$roleId : int
$privilegeId : int
$allowed : bool
Return values
Result

branchFromKey()

First key segment (`:Api:...` gives `Api`); an invalid shape gives `Admin` (fail-safe default; the write validates beforehand anyway).

private branchFromKey(string $key) : string
Parameters
$key : string
Return values
string

buildFlatWhere()

Filters: `branch` (string), `groupId` (int, or `false` for "unassigned"/`IS NULL`; `null` or missing means no group filter), `key` (string, `LIKE`).

private buildFlatWhere(array<string, mixed> $filters) : ParameterType>}
Parameters
$filters : array<string, mixed>
Return values
ParameterType>}

findUnsupportedConditionalGrant()

A new grant of a conditional role on a key whose presenter cannot evaluate that role would be dead (fail-closed deny) and confusing, so the server rejects it even for a hand-forged POST.

private findUnsupportedConditionalGrant(array<int, array{id: ?int, key: string, sort: int, roles: array}> $rows, array<int, string> $conditionalRoles) : string|null

A grant already in the database (same row, same key, allowed = 1) is left alone: existing dead grants are not changed silently and the administrator can only uncheck them. Outside the :Api branch another rule applies (false is enforced).

Parameters
$rows : array<int, array{id: ?int, key: string, sort: int, roles: array}>
$conditionalRoles : array<int, string>

role id => code

Return values
string|null —

role code:key of the first rejected grant, otherwise null

isExistingGrant()

Is this grant (row with the same key x role, `allowed = 1`) already stored in the database?

private isExistingGrant(int|null $privilegeId, string $key, int $roleId) : bool
Parameters
$privilegeId : int|null
$key : string
$roleId : int
Return values
bool

namesForPrivileges()

Returns the privilege names in the current admin language with one query for a list of ids.

private namesForPrivileges(array<int, int> $privilegeIds, int|null $langId) : array<int, string>

$langId === null (the caller does not know the language) or a missing or empty translation falls back to the default application language (LanguageManager::getDefaultLanguageId()); when that is missing too, the name is an empty string (the template always shows the key next to the name, so this is not a dead end).

Parameters
$privilegeIds : array<int, int>
$langId : int|null
Return values
array<int, string> —

privilegeId => name

relationsForPrivileges()

Returns the relations (role => allowed) for a list of privileges with one query.

private relationsForPrivileges(array<int, int> $privilegeIds) : array<int, array<int, bool>>
Parameters
$privilegeIds : array<int, int>
Return values
array<int, array<int, bool>> —

privilegeId => (roleId => allowed)

On this page

Search results