CoolCMS API

UserPaymentRepository extends OrmRepository
in package
uses PaymentItemScopeTrait

Abstract ORM repository with dynamic-includes support.

Every concrete repository extends this class.

Table of Contents

Properties

$db  : EntityManagerInterface
$entityManager  : EntityManagerInterface
$entityUpdater  : EntityUpdater
$managerProvider  : ManagerProvider
$transactionManager  : TransactionManager

Methods

__construct()  : mixed
addCreditsForItem()  : Result
Grants credits for a single active item to its user (tariff with `credits` > 0); the grant is additive.
addCreditsForPayment()  : Result
Grants credits for the given payment; the grant is additive.
beginTransaction()  : void
commit()  : void
computeTotalCount()  : int
Counts the total number of records with applied filters.
delete()  : Result
deleteRelations()  : Result
Deletes rows from a relation table by parent FK column.
findPersistedActiveState()  : Result
Returns the currently stored `active` state of an item straight from the DB.
get()  : BaseEntity|null
Finds a single entity by ID with dynamic includes.
getAll()  : array{0: array, 1: int}
getBy()  : BaseEntity|null
Finds a single entity by filters with dynamic includes.
getEntityClass()  : class-string
Returns the entity class name.
getTransactionLevel()  : int
insertRelations()  : Result
Inserts rows into a relation table.
isInTransaction()  : bool
recalcMemberUntilForUsers()  : Result
Recomputes `member_until` of the given users from the latest `validity_datetime` of their active membership items (any tariff with a duration).
rollback()  : void
save()  : Result
selectRelationIds()  : array<int, int>|null
Returns ids of target records linked to the parent in a relation table (for example gallery asset ids).
setTransactionManager()  : void
updateColumns()  : Result
Updates a table using pre-prepared data from a mapper.
updateMemberUntilForUsers()  : Result
Extends `member_until` of the given users to the latest `validity_datetime` of their active membership items (any tariff with a duration).
updateRelations()  : Result
Updates rows in a relation table.
updateValidityDatetimeForPayment()  : Result
Updates `validity_datetime` of the items of the given payment.
applyFilters()  : void
applyIdentifierFilter()  : void
applyIncludes()  : array<int, string>
Applies includes as LEFT JOINs for hydration.
applyOrdering()  : void
dumpDbError()  : void
entityHasMethod()  : bool
Checks whether the entity class has the given method (association).
fetchCompleteEntities()  : array<string|int, mixed>
Second phase: pure hydration.
fetchIdentifiers()  : array<int, array<string|int, mixed>>
fetchSimple()  : array<string|int, mixed>
getAssociationFromMethod()  : string
Derives the association name from a getter method.
getClassMetadata()  : ClassMetadata<string|int, object>
Returns the Doctrine metadata for the entity.
getDefaultIncludes()  : array<string|int, string>
Default includes for this entity.
getEntityManager()  : EntityManagerInterface
Returns the EntityManager.
getJoinedAliases()  : array<string, true>
Returns all aliases already joined in the QueryBuilder.
getPrimaryKey()  : string
Returns the entity primary key.
getPrimaryKeyField()  : string
Returns the primary key field name.
getRelationField()  : string|null
Returns the field name of the relation to the target entity.
getTableAlias()  : string|null
Reads the table alias from the `TableAlias` attribute, escaped against reserved keywords.
hydrateByIdentifiers()  : array<int, mixed>
Loads entities by PK and hydrates includes while avoiding the cartesian explosion caused by parallel ToMany joins.
isJoined()  : bool
Checks whether the alias is already joined in the QueryBuilder.
mergeWithDefaults()  : array<string|int, string>
Merges explicit includes with the defaults.
sortResultsByIdentifiers()  : array<int, mixed>
buildDistinctIdentityExpression()  : string
Returns the expression whose distinct values are the distinct rows of the root entity.
extractIdsFromScalarResult()  : array<int, int|string|array<string, mixed>>
Converts the scalar result of `fetchIdentifiers()` (`{alias}_{field}` keys) to the format the hydrator expects (scalar for a single PK, `[field => value]` for a composite one).
identityProjectionAlias()  : string
Alias of an `IDENTITY()` projection of an association identifier. Deliberately outside the `{alias}_{relation}` namespace JoinHelper uses for JOIN aliases.
paymentScopeParams()  : ArrayParameterType>}
rawSqlParam()  : mixed
Returns the value for a bare SQL parameter (no DBAL type).

Properties

$entityManager

protected EntityManagerInterface $entityManager

Methods

addCreditsForItem()

Grants credits for a single active item to its user (tariff with `credits` > 0); the grant is additive.

public addCreditsForItem(int $itemId) : Result

Call it only on a fresh activation of the item (insert with active=1 or a 0 to 1 transition detected in beforeSave), otherwise credits would be granted twice.

Parameters
$itemId : int
Return values
Result

addCreditsForPayment()

Grants credits for the given payment; the grant is additive.

public addCreditsForPayment(int $paymentId[, array<int, int>|null $itemIds = null ]) : Result

Any tariff with credits > 0 grants them, whatever else it does (a package of time and credits does both). Grants tariff.credits once for every user who has an active credit item in the payment (GROUP BY user and tariff: one grant per payment regardless of the item count). Call it once per paid gateway transaction: the payment layer runs the callback once per transaction, and a further, different transaction of the same payment (money came again) grants the credits again.

There is deliberately no validity_datetime = max(...) condition across the active credit items of the user: the method is called explicitly once per payment, the payment layer guards against replay of the same transaction, and such a condition would only let a concurrent payment with a higher validity silently take the grant from another payment.

$itemIds: see updateValidityDatetimeForPayment().

Visibility: when the payment has an active credit item with a non-zero tariff but the update granted nothing to anyone, the result is a failure (INVALID_STATE, data expected/granted); nothing was granted, so a retry is safe. A partial grant is only logged (a retry would grant the others a second time).

Parameters
$paymentId : int
$itemIds : array<int, int>|null = null
Return values
Result

computeTotalCount()

Counts the total number of records with applied filters.

public computeTotalCount(array<string|int, mixed> $filters) : int
Parameters
$filters : array<string|int, mixed>
Return values
int

deleteRelations()

Deletes rows from a relation table by parent FK column.

public deleteRelations(string $table, string $column, array<string|int, mixed> $parentIds) : Result
Parameters
$table : string

Table name

$column : string

FK column name

$parentIds : array<string|int, mixed>

Parent IDs to delete

Return values
Result

findPersistedActiveState()

Returns the currently stored `active` state of an item straight from the DB.

public findPersistedActiveState(int $itemId) : Result

Raw SQL outside the Doctrine unit of work, used to detect an active 0 to 1 transition in beforeSave before the flush (the managed entity already carries the new values, so find() cannot be used).

The read uses FOR UPDATE inside the transaction that SaveTrait::saveWithHooks() opens before beforeSave, so the row stays locked until commit. Two admins activating the same inactive item are serialized: the second waits and sees active=1, so credits are granted once.

IMPORTANT: a read error is a failure (not null); the caller must not mistake it for "no transition" and silently skip the credit grant.

Success data: ['active' => 0|1|null] (null when the row does not exist); failure DB_ERROR.

Parameters
$itemId : int
Return values
Result

get()

Finds a single entity by ID with dynamic includes.

public get(int $id[, string|array<string|int, mixed> $includes = [] ]) : BaseEntity|null

Delegates to getAll() with a PK filter and limit 1 and shares its two-phase fetch, which avoids the LIMIT+JOIN problem (a join multiplies rows, so LIMIT 1 would cut collection members).

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

e.g. ['translations', 'routes.translations'] or '*' for all

Return values
BaseEntity|null

getAll()

public getAll([array<string|int, mixed>|null $filters = [] ][, array<string|int, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ][, string|array<string|int, mixed> $includes = [] ]) : array{0: array, 1: int}
Parameters
$filters : array<string|int, mixed>|null = []
$orderBys : array<string|int, mixed>|null = null
$offset : int|null = null
$limit : int|null = null
$columns : array<string|int, mixed>|null = null
$computeTotalCount : bool = false
$includes : string|array<string|int, mixed> = []
Return values
array{0: array, 1: int} —

[entities (or $columns rows), total count]

getBy()

Finds a single entity by filters with dynamic includes.

public getBy([array<string|int, mixed>|null $filters = null ][, string|array<string|int, mixed> $includes = [] ]) : BaseEntity|null

Delegates to getAll() with limit 1 and shares its two-phase fetch, which avoids the LIMIT+JOIN problem (a join multiplies rows, so LIMIT 1 would cut collection members).

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

e.g. ['translations', 'routes.translations'] or '*' for all

Return values
BaseEntity|null

getEntityClass()

Returns the entity class name.

public getEntityClass() : class-string
Return values
class-string

getTransactionLevel()

public getTransactionLevel() : int
Return values
int

insertRelations()

Inserts rows into a relation table.

public insertRelations(string $table, array<int, array<string, mixed>> $rows) : Result
Parameters
$table : string

Table name

$rows : array<int, array<string, mixed>>

Array of ['column_name' => value] rows

Return values
Result

recalcMemberUntilForUsers()

Recomputes `member_until` of the given users from the latest `validity_datetime` of their active membership items (any tariff with a duration).

public recalcMemberUntilForUsers(array<string|int, int> $userIds) : Result

Unlike updateMemberUntilForUsers it can also shorten the value, and it sets it to null when no active membership item remains (LEFT JOIN). For the admin path (deactivation, shorter validity, deleting a membership item); do not use it on the gateway path, which is monotonic on purpose. Credits are not touched; they are additive and irreversible.

NOTE: it takes the max of the stored validity_datetime values. Deleting a middle stacked membership item shortens the value only approximately, because the validity of the remaining items is not recomputed; deleting the last item is exact.

Parameters
$userIds : array<string|int, int>
Return values
Result

save()

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

Field names to skip during UPDATE (e.g. ['userRoles'])

Return values
Result

selectRelationIds()

Returns ids of target records linked to the parent in a relation table (for example gallery asset ids).

public selectRelationIds(string $table, string $parentColumn, int $parentId, string $targetColumn) : array<int, int>|null

Soft-removed rows are read too, as they still belong to the same parent. On a database error it returns null; the caller decides, and a foreign id must then not be accepted.

Parameters
$table : string
$parentColumn : string
$parentId : int
$targetColumn : string
Return values
array<int, int>|null

updateColumns()

Updates a table using pre-prepared data from a mapper.

public updateColumns(array<string, mixed> $set, array<int, array<string, array{column: string, value: mixed}>> $where, string $table) : Result
Parameters
$set : array<string, mixed>

['column_name' => value|SqlValue]

$where : array<int, array<string, array{column: string, value: mixed}>>

[[ 'paramName' => ['column'=>col, 'value'=>val], ... ], ... ]

$table : string

table name

Return values
Result

updateMemberUntilForUsers()

Extends `member_until` of the given users to the latest `validity_datetime` of their active membership items (any tariff with a duration).

public updateMemberUntilForUsers(array<string|int, int> $userIds) : Result

Idempotent and monotonic (it only extends), so it is safe to call from both the gateway and the admin save.

Parameters
$userIds : array<string|int, int>
Return values
Result

updateRelations()

Updates rows in a relation table.

public updateRelations(string $table, array<int, array{set: array, where: array}> $updates) : Result
Parameters
$table : string

Table name

$updates : array<int, array{set: array, where: array}>

Array of ['set' => [col => val], 'where' => [col => val]]

Return values
Result

updateValidityDatetimeForPayment()

Updates `validity_datetime` of the items of the given payment.

public updateValidityDatetimeForPayment(int $paymentId[, array<int, int>|null $itemIds = null ]) : Result

A tariff with a duration (duration > 0, in days) extends membership: the new period is added to the end of the running membership (the user's member_until, or the latest validity of their active time items when that is later), or starts now when the membership has already expired. Every time tariff stacks on the same end, so the tariff id plays no role. An item that is already active (a further paid transaction of the same payment) counts with its own validity too, so paying again adds another period instead of recomputing the same one.

A tariff without a duration (credits only) gets NOW(): the purchase moment, no membership.

$itemIds limits the update to those items; null means all items of the payment; an empty list updates nothing.

Parameters
$paymentId : int
$itemIds : array<int, int>|null = null
Return values
Result

applyFilters()

protected applyFilters(QueryBuilder $qb, array<string|int, mixed>|null $filters, class-string $entityClass, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$filters : array<string|int, mixed>|null
$entityClass : class-string
$fromTableAlias : string

applyIdentifierFilter()

protected applyIdentifierFilter(QueryBuilder $qb, array<int, array<string|int, mixed>> $identifiers, array<int, string> $identifierFields, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$identifiers : array<int, array<string|int, mixed>>
$identifierFields : array<int, string>
$fromTableAlias : string

applyIncludes()

Applies includes as LEFT JOINs for hydration.

protected applyIncludes(QueryBuilder $qb, array<string|int, mixed> $includes, string $alias[, array<string|int, mixed> &$joinedAliases = [] ][, class-string|null $entityClass = null ]) : array<int, string>

Hydration only: no WHERE and no WITH conditions. Filters are applied in the first phase (fetchIdentifiers()) over ids; the second phase only loads the full collections for the selected entities.

Parameters
$qb : QueryBuilder

Query builder

$includes : array<string|int, mixed>

Includes, e.g. ['translations', 'routes.translations']

$alias : string

Alias of the root entity

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

Reference tracking already-joined aliases

$entityClass : class-string|null = null

Root entity for resolving includes (default: root repo entity)

Return values
array<int, string> —

Aliases for the SELECT

applyOrdering()

protected applyOrdering(QueryBuilder $qb, array<string|int, mixed>|null $orderBys, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$orderBys : array<string|int, mixed>|null
$fromTableAlias : string

dumpDbError()

protected dumpDbError(Throwable $e) : void
Parameters
$e : Throwable

entityHasMethod()

Checks whether the entity class has the given method (association).

protected entityHasMethod(string $entityClass, string $method) : bool
Parameters
$entityClass : string
$method : string

Method name, e.g. 'getTranslations'

Return values
bool

fetchCompleteEntities()

Second phase: pure hydration.

protected fetchCompleteEntities(class-string $entityClass, string $fromTableAlias, array<int, array<string|int, mixed>> $identifiers, array<string|int, mixed>|null $columns, array<string|int, mixed> $includes[, array<string|int, mixed>|null $filters = null ]) : array<string|int, mixed>

Delegates to the shared hydrateByIdentifiers() of OrmRepository, which loads to-many branches separately via WHERE parent_id IN (:ids) (no cartesian explosion from parallel to-many joins).

Parameters
$entityClass : class-string
$fromTableAlias : string
$identifiers : array<int, array<string|int, mixed>>
$columns : array<string|int, mixed>|null
$includes : array<string|int, mixed>
$filters : array<string|int, mixed>|null = null
Return values
array<string|int, mixed>

fetchIdentifiers()

protected fetchIdentifiers(class-string $entityClass, string $fromTableAlias, array<string|int, mixed>|null $filters, array<string|int, mixed>|null $orderBys, int|null $offset, int|null $limit) : array<int, array<string|int, mixed>>
Parameters
$entityClass : class-string
$fromTableAlias : string
$filters : array<string|int, mixed>|null
$orderBys : array<string|int, mixed>|null
$offset : int|null
$limit : int|null
Return values
array<int, array<string|int, mixed>> —

scalar rows keyed {alias}_{field}

fetchSimple()

protected fetchSimple(class-string $entityClass, string $fromTableAlias, array<string|int, mixed>|null $filters, array<string|int, mixed>|null $orderBys, int|null $offset, int|null $limit, array<string|int, mixed>|null $columns) : array<string|int, mixed>
Parameters
$entityClass : class-string
$fromTableAlias : string
$filters : array<string|int, mixed>|null
$orderBys : array<string|int, mixed>|null
$offset : int|null
$limit : int|null
$columns : array<string|int, mixed>|null
Return values
array<string|int, mixed>

getAssociationFromMethod()

Derives the association name from a getter method.

protected getAssociationFromMethod(string $method) : string

E.g. 'getTranslations' → 'translations'.

Parameters
$method : string
Return values
string

getClassMetadata()

Returns the Doctrine metadata for the entity.

protected getClassMetadata() : ClassMetadata<string|int, object>
Return values
ClassMetadata<string|int, object>

getDefaultIncludes()

Default includes for this entity.

protected getDefaultIncludes() : array<string|int, string>

Override in a child, or place the DefaultIncludes attribute on the entity.

Return values
array<string|int, string>

getEntityManager()

Returns the EntityManager.

protected getEntityManager() : EntityManagerInterface
Return values
EntityManagerInterface

getJoinedAliases()

Returns all aliases already joined in the QueryBuilder.

protected getJoinedAliases(QueryBuilder $qb) : array<string, true>
Parameters
$qb : QueryBuilder
Return values
array<string, true>

getPrimaryKey()

Returns the entity primary key.

protected getPrimaryKey() : string
Return values
string

getPrimaryKeyField()

Returns the primary key field name.

protected getPrimaryKeyField() : string
Return values
string

getRelationField()

Returns the field name of the relation to the target entity.

protected getRelationField(class-string $entityClass, string $targetEntity) : string|null
Parameters
$entityClass : class-string
$targetEntity : string
Return values
string|null

getTableAlias()

Reads the table alias from the `TableAlias` attribute, escaped against reserved keywords.

protected getTableAlias(class-string $entityClass) : string|null

The attribute value is used verbatim as the QueryBuilder identification variable and FROM alias (e.g. ->createQueryBuilder('order')), and the query lexer treats a bare reserved word as a keyword token instead of an identifier, which breaks the parser. Escaping here, the single choke point that find() and getAll() funnel through, protects every present and future entity without hand-picking alias strings per #[TableAlias] declaration.

Parameters
$entityClass : class-string
Return values
string|null

hydrateByIdentifiers()

Loads entities by PK and hydrates includes while avoiding the cartesian explosion caused by parallel ToMany joins.

protected hydrateByIdentifiers(class-string $entityClass, string $fromTableAlias, array<string|int, int|string|array<string, mixed>> $ids, array<string|int, mixed> $includes[, array<string|int, mixed>|null $columns = null ][, array<string|int, mixed>|null $filters = null ]) : array<int, mixed>

The root query holds only ToOne chains (no DISTINCT / temp table). Every ToMany branch is loaded separately via WHERE parent_id IN (:ids) and the collections are then stitched back onto the parent entities in PHP.

Filters (if passed) propagate into the ToMany branches only when they opt in with BaseFilter::setApplyToCollections(true): such a filter with the dot-path A.B.C is applied to every branch whose path is a prefix of it (with matching trimming). Other filters, and those with isPhaseOneOnly() === true, stay purely in phase 1, so included collections are loaded complete. See filtersForBranch().

Parameters
$entityClass : class-string

Root entity

$fromTableAlias : string

alias of the root entity in the query

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

PK list: either scalar (single-column PK), or [field => value] (composite)

$includes : array<string|int, mixed>

Dot-notation includes, e.g. ['routes.translations', 'parameterRels.parameter']

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

Column restriction on the root entity

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

Phase 1 filters (for propagation into ToMany branches)

Return values
array<int, mixed> —

entities (or column rows when $columns is given)

isJoined()

Checks whether the alias is already joined in the QueryBuilder.

protected isJoined(QueryBuilder $qb, string $alias) : bool
Parameters
$qb : QueryBuilder
$alias : string
Return values
bool

mergeWithDefaults()

Merges explicit includes with the defaults.

protected mergeWithDefaults(string|array<string|int, string> $includes) : array<string|int, string>

Passing '*' loads every association of the entity.

Parameters
$includes : string|array<string|int, string>

Explicit includes, or '*' for all

Return values
array<string|int, string> —

Merged includes

sortResultsByIdentifiers()

protected sortResultsByIdentifiers(array<string|int, mixed> $results, array<int, array<string|int, mixed>> $identifiers, class-string $entityClass, string $fromTableAlias) : array<int, mixed>
Parameters
$results : array<string|int, mixed>
$identifiers : array<int, array<string|int, mixed>>
$entityClass : class-string
$fromTableAlias : string
Return values
array<int, mixed>

buildDistinctIdentityExpression()

Returns the expression whose distinct values are the distinct rows of the root entity.

private buildDistinctIdentityExpression(ClassMetadata<string|int, object> $metadata, string $alias) : string

IMPORTANT: every identifier component must be counted. Counting only the first one makes a compound identity (CategoryText = languageId + parent) return the number of distinct languages instead of rows. DQL has no multi-column COUNT(DISTINCT), so the components are joined with a separator; an association component goes through IDENTITY() (its FK), a scalar one directly. CONCAT is portable (the SQLite platform renders ||), and the separator cannot occur inside integer keys.

Parameters
$metadata : ClassMetadata<string|int, object>
$alias : string
Return values
string

extractIdsFromScalarResult()

Converts the scalar result of `fetchIdentifiers()` (`{alias}_{field}` keys) to the format the hydrator expects (scalar for a single PK, `[field => value]` for a composite one).

private extractIdsFromScalarResult(array<int, array<string|int, mixed>> $identifiers, array<int, string> $identifierFields, string $fromTableAlias) : array<int, int|string|array<string, mixed>>
Parameters
$identifiers : array<int, array<string|int, mixed>>
$identifierFields : array<int, string>
$fromTableAlias : string
Return values
array<int, int|string|array<string, mixed>>

identityProjectionAlias()

Alias of an `IDENTITY()` projection of an association identifier. Deliberately outside the `{alias}_{relation}` namespace JoinHelper uses for JOIN aliases.

private static identityProjectionAlias(string $fromTableAlias, string $idField) : string
Parameters
$fromTableAlias : string
$idField : string
Return values
string

paymentScopeParams()

private static paymentScopeParams(int $paymentId, array<int, int>|null $itemIds) : ArrayParameterType>}
Parameters
$paymentId : int
$itemIds : array<int, int>|null
Return values
ArrayParameterType>}

rawSqlParam()

Returns the value for a bare SQL parameter (no DBAL type).

private rawSqlParam(mixed $value) : mixed

The mysqli driver would send false as '' and cannot bind a DateTime at all; a database in strict mode then rejects the write ("Incorrect integer value"), while a non-strict one silently converts it to 0.

Parameters
$value : mixed
On this page

Search results