CoolCMS API

ApiClientService extends Service
in package
uses SaveTrait

Clients of the external Api and their keys, plus a cache of the client record by key digest.

Key verification runs on every external request: the record is loaded once per request and kept between requests by the Nette cache storage (Redis, files without Redis) for at most api.clientKey.cacheTtl seconds. Every client or key change through the manager deletes the records of all keys of the client (forgetClient()), so disabling, revoking and a new expiry apply immediately. The cache does not watch key expiry; the manager decides from the request time.

An unknown key is not cached: random keys would fill the cache; it costs one query on a unique index.

The class is not final so that unit tests can replace it with a mock in the manager (the project has no bypass-finals).

Tags
phpstan-type

ApiClientSnapshot array{id: int, name: string, enabled: bool, revoked: bool, roleCodes: list, limitPerSecond: int|null, limitPerMinute: int|null, userLimitPerMinute: int|null, ipLimitPerMinute: int|null, lastUsedAt: int|null, keyId: int, keyExpiresAt: int|null, keyRevoked: bool, keyLastUsedAt: int|null}

ApiClientValues array{name: string, notes: string|null, limitPerSecond: int|null, limitPerMinute: int|null, userLimitPerMinute: int|null, ipLimitPerMinute: int|null}

ApiClientRow array{id: int, name: string, enabled: bool, revoked: bool, createdAt: string|null, updatedAt: string|null, revokedAt: string|null, lastUsedAt: string|null, notes: string|null, limitPerSecond: int|null, limitPerMinute: int|null, userLimitPerMinute: int|null, ipLimitPerMinute: int|null}

ApiClientKeyRow array{id: int, keyPrefix: string, label: string|null, createdAt: string|null, expiresAt: string|null, revokedAt: string|null, lastUsedAt: string|null}

extends

Table of Contents

Constants

CACHE_NAMESPACE  : string = 'apiClient.byKeyHash'
MAX_CACHE_TTL  : int = 60
Maximum allowed cache lifetime in seconds.

Properties

$mapper  : MapperInterface
$apiClientKeyConfig  : ApiClientKeyConfig
$apiClientMapper  : ApiClientMapper
$cache  : Cache
$requestCache  : array<string, ApiClientSnapshot>

Methods

__construct()  : mixed
beginTransaction()  : void
commit()  : void
count()  : int
Count all entities matching filters.
countValidKeys()  : int
delete()  : Result
Delete entity by ID.
deleteAll()  : Result
Delete multiple entities by IDs.
deleteBy()  : Result
Delete entity by filters.
deleteKey()  : int
exists()  : bool
Check if entity exists by ID.
existsBy()  : bool
Check if entity exists by filters.
fetchKeyHashes()  : array<int, string>
fetchKeys()  : array<int, array<int, ApiClientKeyRow>>
fetchRoleIds()  : array<int, array<int, int>>
fetchRoleOptions()  : array<int, array{id: int, code: string, name: string, implicit: string, conditional: bool, privileged: bool}>
fetchRows()  : array<int, ApiClientRow>
find()  : T|null
Find entity by ID.
findAll()  : T>, 1: int} : array{0: list>, 1: int})
Find all entities with optional filters and includes.
findAllBy()  : array<int, T>
Find entities by filters.
findBy()  : T|null
Find one entity by filters.
findByOrFail()  : T
Find one entity by filters or throw exception.
findOrFail()  : T
Find entity by ID or throw exception.
findRelatedIds()  : array<int, int>|null
Returns ids of target entities linked to the parent through the relation collection (see the mapper).
findSnapshotByKeyHash()  : ApiClientSnapshot|null
Returns the client record by key digest: `id`, `name`, `enabled`, `revoked`, `roleCodes`, limits, `lastUsedAt` (unix time) and the key state (`keyId`, `keyExpiresAt`, `keyRevoked`, `keyLastUsedAt`).
forgetClient()  : void
Forgets the records of all keys of the client; the manager calls it after every client or key change (each key record carries the client state too). The caller must forget a deleted key separately while it still knows its digest (after the `DELETE` this method cannot find it).
forgetKeyHash()  : void
Forgets the record of one key.
getTransactionLevel()  : int
insertClient()  : int
insertKey()  : int
isInTransaction()  : bool
rememberSnapshot()  : void
Stores the record in the cache (also after writing `last_used_at`, so later requests within the interval do not write again).
replaceRoles()  : void
resolveEntityClass()  : TEntity>
Exposes the root entity class of the service publicly.
revokeKey()  : int
rollback()  : void
save()  : Result
Save entity.
saveAll()  : Result
Save multiple entities at once.
touchLastUsed()  : int
updateAllRelations()  : Result
Update all relations of parent entity with given values.
updateClient()  : int
updateColumns()  : Result
Update specific columns.
updateEnabled()  : int
updateKey()  : int
updateRelations()  : Result
Update existing relation entities via Mapper.
updateRevoked()  : int
deleteWithoutCache()  : Result
Delete entity from database via Mapper.
findAllFromDb()  : T>, 1: int} : array{0: list>, 1: int})
Load all entities from database via Mapper → Repository.
findFromDb()  : T|null
Load entity from database via Mapper.
getEntityClass()  : TEntity>
Returns the service entity class; every concrete service returns `Entity::class` matching its `@extends Service<Entity>`.
narrowEntities()  : array<int, TEntity>
narrowEntity()  : TEntity|null
Narrows the generic {@see BaseEntity} returned by the mapper to the service entity.
narrowRows()  : array<int, array<string, mixed>>
Narrows query rows for `$columns` (no hydration); every item must be an array keyed by column names.
saveWithoutCache()  : Result
Save entity to database via Mapper.
updateColumnsWithoutCache()  : Result
Update columns in database via Mapper.
cacheTtl()  : int
isTechnicalDeleteFailure()  : bool
Technical failure = the persistence layer broke (exception attached or a technical code); the EntityManager may be closed and no further write may be attempted.

Constants

MAX_CACHE_TTL

Maximum allowed cache lifetime in seconds.

private int MAX_CACHE_TTL = 60

Properties

$requestCache

private array<string, ApiClientSnapshot> $requestCache = []

key digest => record, for this request only

Methods

count()

Count all entities matching filters.

public count([array<string|int, mixed> $filters = [] ][, bool $useCache = true ]) : int
Parameters
$filters : array<string|int, mixed> = []

Filter conditions

$useCache : bool = true

kept for backward compatibility; no effect here

Return values
int —

Total count

countValidKeys()

public countValidKeys(int $clientId, DateTimeInterface $now) : int
Parameters
$clientId : int
$now : DateTimeInterface
Return values
int

delete()

Delete entity by ID.

public delete(int|array<string|int, mixed> $id) : Result

IMPORTANT: an array means a list of ids, not a composite key (Mapper::find() takes only int). Without this redirect to deleteAll() every bulk delete from the DataGrid ends with TypeError: Mapper::find(): Argument #1 ($id) must be of type int, array given (the grid calls Manager::delete($ids) with the array of selected rows).

Parameters
$id : int|array<string|int, mixed>

Entity ID to delete

Return values
Result —

Success if deleted, Error if entity not found

deleteAll()

Delete multiple entities by IDs.

public deleteAll(array<string|int, mixed> $ids) : Result

Partial success is the contract for domain refusals (missing id, guard): the loop continues and the errors are listed next to deletedCount. A technical failure (DB_ERROR and similar, see ResultCode::isTechnical()) aborts the loop instead: after a failed ORM flush Doctrine closes the EntityManager, so the next remove() would throw EntityManagerClosed out of the loop and the caller would get no summary of the rows already deleted (durable in autocommit). The failed Result carries deletedIds, failedId and remainingIds; the caller decides whether the accepted prefix stays (autocommit) or is rolled back (the manager bulk paths run inside a transaction and roll it back).

Parameters
$ids : array<string|int, mixed>

entity ids (int or numeric string); for a composite key an array [property => value]

Return values
Result —

Success with delete count, or Error

deleteBy()

Delete entity by filters.

public deleteBy(array<string|int, mixed> $filters) : Result
Parameters
$filters : array<string|int, mixed>

Filter conditions

Return values
Result —

Success if deleted, Error if not found

deleteKey()

public deleteKey(int $clientId, int $keyId, DateTimeInterface $now) : int
Parameters
$clientId : int
$keyId : int
$now : DateTimeInterface
Return values
int

exists()

Check if entity exists by ID.

public exists(int|array<string|int, mixed> $id[, bool $useCache = true ]) : bool
Parameters
$id : int|array<string|int, mixed>

Entity ID

$useCache : bool = true

kept for backward compatibility; no effect here

Return values
bool —

True if entity exists

existsBy()

Check if entity exists by filters.

public existsBy(array<string|int, mixed> $filters[, bool $useCache = true ]) : bool
Parameters
$filters : array<string|int, mixed>

Filter conditions

$useCache : bool = true

kept for backward compatibility; no effect here

Return values
bool —

True if entity exists

fetchKeyHashes()

public fetchKeyHashes(int $clientId) : array<int, string>
Parameters
$clientId : int
Return values
array<int, string> —

key digests of the client; for the cache only, never leave the Api

fetchKeys()

public fetchKeys(array<int, int> $clientIds) : array<int, array<int, ApiClientKeyRow>>
Parameters
$clientIds : array<int, int>
Return values
array<int, array<int, ApiClientKeyRow>>

fetchRoleIds()

public fetchRoleIds(array<int, int> $clientIds) : array<int, array<int, int>>
Parameters
$clientIds : array<int, int>
Return values
array<int, array<int, int>>

fetchRoleOptions()

public fetchRoleOptions() : array<int, array{id: int, code: string, name: string, implicit: string, conditional: bool, privileged: bool}>
Return values
array<int, array{id: int, code: string, name: string, implicit: string, conditional: bool, privileged: bool}>

fetchRows()

public fetchRows([int|null $clientId = null ]) : array<int, ApiClientRow>
Parameters
$clientId : int|null = null
Return values
array<int, ApiClientRow>

find()

Find entity by ID.

public find(int|array<string|int, mixed> $id[, array<string|int, mixed> $includes = [] ][, bool $useCache = true ]) : T|null
Parameters
$id : int|array<string|int, mixed>

Entity ID (scalar for simple keys, array for composite keys)

$includes : array<string|int, mixed> = []

Relations to load (e.g., ['translations', 'user.roles'])

$useCache : bool = true

kept for backward compatibility (caching is done in the presenter); no effect here

Return values
T|null —

Entity instance or null if not found

findAll()

Find all entities with optional filters and includes.

public findAll([array<string|int, mixed> $includes = [] ][, array<string|int, mixed> $filters = [] ][, array<string|int, mixed>|null $orderBy = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ][, bool $useCache = true ]) : T>, 1: int} : array{0: list>, 1: int})
Parameters
$includes : array<string|int, mixed> = []

Relations to load (e.g., ['translations', 'user.roles'])

$filters : array<string|int, mixed> = []

Filter conditions (e.g., ['activated' => 1])

$orderBy : array<string|int, mixed>|null = null

Sorting (e.g., ['createdAt' => 'DESC'])

$offset : int|null = null

Offset for pagination

$limit : int|null = null

Limit number of results

$columns : array<string|int, mixed>|null = null

Columns to select (null = all)

$computeTotalCount : bool = false

Whether to compute total count for pagination

$useCache : bool = true

kept for backward compatibility (caching is done in the presenter); no effect here

Return values
T>, 1: int} : array{0: list>, 1: int}) —

Tuple: [entities (or $columns rows), totalCount]

findAllBy()

Find entities by filters.

public findAllBy(array<string|int, mixed> $filters[, array<string|int, mixed> $includes = [] ][, array<string|int, mixed>|null $order = null ][, int|null $offset = null ][, int|null $limit = null ][, bool $useCache = true ]) : array<int, T>
Parameters
$filters : array<string|int, mixed>

Filter conditions (e.g., ['activated' => 1, 'userId' => 5])

$includes : array<string|int, mixed> = []

Relations to load

$order : array<string|int, mixed>|null = null

Sorting

$offset : int|null = null

Offset

$limit : int|null = null

Limit

$useCache : bool = true

kept for backward compatibility; no effect here

Return values
array<int, T> —

Array of entities

findBy()

Find one entity by filters.

public findBy(array<string|int, mixed> $filters[, array<string|int, mixed> $includes = [] ][, bool $useCache = true ]) : T|null
Parameters
$filters : array<string|int, mixed>

Filter conditions

$includes : array<string|int, mixed> = []

Relations to load

$useCache : bool = true

kept for backward compatibility; no effect here

Return values
T|null —

Entity or null if not found

findByOrFail()

Find one entity by filters or throw exception.

public findByOrFail(array<string|int, mixed> $filters[, array<string|int, mixed> $includes = [] ][, bool $useCache = true ]) : T
Parameters
$filters : array<string|int, mixed>

Filter conditions

$includes : array<string|int, mixed> = []

Relations to load

$useCache : bool = true

kept for backward compatibility; no effect here

Tags
throws
RuntimeException

If entity not found

Return values
T —

Entity instance

findOrFail()

Find entity by ID or throw exception.

public findOrFail(int|array<string|int, mixed> $id[, array<string|int, mixed> $includes = [] ][, bool $useCache = true ]) : T
Parameters
$id : int|array<string|int, mixed>

Entity ID

$includes : array<string|int, mixed> = []

Relations to load

$useCache : bool = true

kept for backward compatibility; no effect here

Tags
throws
RuntimeException

If entity not found

Return values
T —

Entity instance

findRelatedIds()

Returns ids of target entities linked to the parent through the relation collection (see the mapper).

public findRelatedIds(int $parentId, string $property, string $targetAssociation) : array<int, int>|null
Parameters
$parentId : int
$property : string
$targetAssociation : string
Return values
array<int, int>|null

findSnapshotByKeyHash()

Returns the client record by key digest: `id`, `name`, `enabled`, `revoked`, `roleCodes`, limits, `lastUsedAt` (unix time) and the key state (`keyId`, `keyExpiresAt`, `keyRevoked`, `keyLastUsedAt`).

public findSnapshotByKeyHash(string $keyHash) : ApiClientSnapshot|null

Null when the key does not exist.

Parameters
$keyHash : string
Return values
ApiClientSnapshot|null

forgetClient()

Forgets the records of all keys of the client; the manager calls it after every client or key change (each key record carries the client state too). The caller must forget a deleted key separately while it still knows its digest (after the `DELETE` this method cannot find it).

public forgetClient(int $clientId) : void
Parameters
$clientId : int

forgetKeyHash()

Forgets the record of one key.

public forgetKeyHash(string $keyHash) : void
Parameters
$keyHash : string

getTransactionLevel()

public getTransactionLevel() : int
Return values
int

insertClient()

public insertClient(ApiClientValues $values, DateTimeInterface $now) : int
Parameters
$values : ApiClientValues
$now : DateTimeInterface
Return values
int

insertKey()

public insertKey(int $clientId, string $keyHash, string $keyPrefix, string|null $label, DateTimeInterface|null $expiresAt, DateTimeInterface $now, int $maxValidKeys) : int
Parameters
$clientId : int
$keyHash : string
$keyPrefix : string
$label : string|null
$expiresAt : DateTimeInterface|null
$now : DateTimeInterface
$maxValidKeys : int
Return values
int —

id of the new key; 0 when the client is revoked or already has the full ceiling of valid keys

rememberSnapshot()

Stores the record in the cache (also after writing `last_used_at`, so later requests within the interval do not write again).

public rememberSnapshot(string $keyHash, ApiClientSnapshot $snapshot) : void
Parameters
$keyHash : string
$snapshot : ApiClientSnapshot

replaceRoles()

public replaceRoles(int $clientId, array<int, int> $roleIds) : void
Parameters
$clientId : int
$roleIds : array<int, int>

resolveEntityClass()

Exposes the root entity class of the service publicly.

public resolveEntityClass() : TEntity>

getEntityClass() is protected in the traits (implemented per service); this method makes it available to the manager so that the write endpoint takes the entity from the presenter, not from the request.

Return values
TEntity>

revokeKey()

public revokeKey(int $clientId, int $keyId, DateTimeInterface $now) : int
Parameters
$clientId : int
$keyId : int
$now : DateTimeInterface
Return values
int

save()

Save entity.

public save(TEntity $entity[, array<int, string> $skipFields = [] ]) : Result
Parameters
$entity : TEntity

Entity to save

$skipFields : array<int, string> = []

Field names to skip during UPDATE

Return values
Result —

Success/error result

saveAll()

Save multiple entities at once.

public saveAll(array<string|int, TEntity> $entities) : Result
Parameters
$entities : array<string|int, TEntity>

Array of entities to save

Return values
Result —

Success with saved count, or Error

touchLastUsed()

public touchLastUsed(int $clientId, int $keyId, DateTimeInterface $now, int $intervalSeconds) : int
Parameters
$clientId : int
$keyId : int
$now : DateTimeInterface
$intervalSeconds : int
Return values
int

updateAllRelations()

Update all relations of parent entity with given values.

public updateAllRelations(array<string|int, mixed> $entityIds, string $property, array<string, mixed> $values) : Result
Parameters
$entityIds : array<string|int, mixed>
$property : string
$values : array<string, mixed>
Return values
Result

updateClient()

public updateClient(int $clientId, ApiClientValues $values, DateTimeInterface $now) : int
Parameters
$clientId : int
$values : ApiClientValues
$now : DateTimeInterface
Return values
int

updateColumns()

Update specific columns.

public updateColumns(array<string|int, mixed> $entityIds, array<string, mixed> $values) : Result
Parameters
$entityIds : array<string|int, mixed>

Entity IDs (scalar or composite-id arrays)

$values : array<string, mixed>

Column values to update (property name => value)

Return values
Result —

Success/error result

updateEnabled()

public updateEnabled(int $clientId, bool $enabled, DateTimeInterface $now) : int
Parameters
$clientId : int
$enabled : bool
$now : DateTimeInterface
Return values
int

updateKey()

public updateKey(int $clientId, int $keyId, string|null $label, DateTimeInterface|null $expiresAt) : int
Parameters
$clientId : int
$keyId : int
$label : string|null
$expiresAt : DateTimeInterface|null
Return values
int

updateRelations()

Update existing relation entities via Mapper.

public updateRelations(array<string|int, mixed> $entityIds, string $property, array<string|int, object> $relationEntities) : Result
Parameters
$entityIds : array<string|int, mixed>
$property : string
$relationEntities : array<string|int, object>
Return values
Result

updateRevoked()

public updateRevoked(int $clientId, DateTimeInterface $now) : int
Parameters
$clientId : int
$now : DateTimeInterface
Return values
int

deleteWithoutCache()

Delete entity from database via Mapper.

protected deleteWithoutCache(BaseEntity $entity) : Result

Called internally by delete() and deleteAll().

Parameters
$entity : BaseEntity
Return values
Result

findAllFromDb()

Load all entities from database via Mapper → Repository.

protected findAllFromDb([array<string|int, mixed> $includes = [] ][, array<string|int, mixed> $filters = [] ][, array<string|int, mixed>|null $orderBy = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ]) : T>, 1: int} : array{0: list>, 1: int})
Parameters
$includes : array<string|int, mixed> = []

Relations to load

$filters : array<string|int, mixed> = []

Filter conditions

$orderBy : array<string|int, mixed>|null = null

Sorting

$offset : int|null = null

Offset

$limit : int|null = null

Limit

$columns : array<string|int, mixed>|null = null

Columns to select

$computeTotalCount : bool = false

Whether to compute total count

Return values
T>, 1: int} : array{0: list>, 1: int}) —

Tuple: [entities (or $columns rows), totalCount]

findFromDb()

Load entity from database via Mapper.

protected findFromDb(int|array<string|int, mixed> $id, array<string|int, mixed> $includes) : T|null
Parameters
$id : int|array<string|int, mixed>

Entity ID

$includes : array<string|int, mixed>

Relations to load

Return values
T|null —

Entity or null

getEntityClass()

Returns the service entity class; every concrete service returns `Entity::class` matching its `@extends Service<Entity>`.

protected getEntityClass() : TEntity>
Return values
TEntity>

narrowEntities()

protected narrowEntities(array<string|int, mixed> $items) : array<int, TEntity>
Parameters
$items : array<string|int, mixed>
Return values
array<int, TEntity>

narrowEntity()

Narrows the generic {@see BaseEntity} returned by the mapper to the service entity.

protected narrowEntity(BaseEntity|null $entity) : TEntity|null

The service owns a single entity (getEntityClass()), so a foreign class means a wiring error (wrong mapper), not a normal state.

Parameters
$entity : BaseEntity|null
Return values
TEntity|null

narrowRows()

Narrows query rows for `$columns` (no hydration); every item must be an array keyed by column names.

protected narrowRows(array<string|int, mixed> $items) : array<int, array<string, mixed>>
Parameters
$items : array<string|int, mixed>
Return values
array<int, array<string, mixed>>

saveWithoutCache()

Save entity to database via Mapper.

protected saveWithoutCache(BaseEntity $entity[, array<int, string> $skipFields = [] ]) : Result
Parameters
$entity : BaseEntity
$skipFields : array<int, string> = []
Return values
Result

updateColumnsWithoutCache()

Update columns in database via Mapper.

protected updateColumnsWithoutCache(array<string|int, mixed> $entityIds, array<string, mixed> $values) : Result
Parameters
$entityIds : array<string|int, mixed>
$values : array<string, mixed>
Return values
Result

isTechnicalDeleteFailure()

Technical failure = the persistence layer broke (exception attached or a technical code); the EntityManager may be closed and no further write may be attempted.

private isTechnicalDeleteFailure(Result $result) : bool
Parameters
$result : Result
Return values
bool
On this page

Search results