CoolCMS API

WatchdogEmailRelRepository extends OrmRepository
in package

Abstract ORM repository with dynamic-includes support.

Every concrete repository extends this class.

Table of Contents

Constants

FANOUT_TRIGGER_NAME  : mixed = 'cms_mod_bazaar_watchdog_email_texts_AFTER_INSERT'
Name of the database trigger that used to do the watchdog fan-out into the queue (after insert on `*_watchdog_email_texts`).
DIGEST_MAX_AGE_DAYS  : int = 2
The digest takes pending rows only for adverts inserted within the last N days.

Properties

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

Methods

__construct()  : mixed
beginTransaction()  : void
buildFanoutSelectSql()  : string
Builds the fan-out select (separate so it is unit-testable without the entity manager or a database).
claimSent()  : bool
Claims a pending relation for sending: `sent` 0 to 1 in one conditional UPDATE, so of two concurrent cron runs only one gets it. Returns false when the row was already claimed, sent or cancelled; the caller then skips it. A failed send gives the claim back ({@see self::releaseClaim()}).
commit()  : void
computeTotalCount()  : int
Counts the total number of records with applied filters.
delete()  : Result
deleteForAdverts()  : int
Hard delete of adverts: deletes the watchdog queue for the e-mails of the given adverts before the cascade from the advert reaches `cms_mod_bazaar_watchdog_emails`.
deleteRelations()  : Result
Deletes rows from a relation table by parent FK column.
fanoutTriggerExists()  : bool
Tells whether the database fan-out trigger still exists.
fetchFanoutRecipients()  : array<int, array{to: string|null, from: string|null, subject: string|null, body: string|null}>
Reads the fan-out recipients, the PHP replacement of the database trigger `cms_mod_bazaar_watchdog_email_texts_AFTER_INSERT`.
fetchPending()  : array<int, array{watchdog_email_id: int, watchdog_id: int, attempts: int, from: string, watchdog_email: string|null, user_id: int|null, subject: string, body: string}>
Fetches pending rows for immediate sending (mode 1).
fetchPendingForDigest()  : array<int, array{watchdog_email_id: int, watchdog_id: int, advert_id: int, from: string, watchdog_email: string|null, user_id: int|null, language_id: int}>
Fetches pending rows for the digest (mode 2).
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
incrementAttempts()  : void
Adds one attempt; the last allowed attempt cancels the row at once.
insertMatchingRels()  : int
Inserts the relations "watchdog e-mail x matching watchdog" (modes 1 and 2, delivery is then done by the crons).
insertRelations()  : Result
Inserts rows into a relation table.
isInTransaction()  : bool
markCancelledBatch()  : void
Takes undeliverable rows (a watchdog without an e-mail and a user without one) out of the pending set.
markEmailRelsSent()  : int
Marks all pending relations of one watchdog e-mail sent (the old database trigger has queued the e-mail already, {@see self::fanoutTriggerExists()}). Returns the number of marked rows.
markSentBatch()  : void
purgeProcessedOlderThan()  : array{rels: int, emails: int}
Applies queue retention.
releaseClaim()  : void
Gives a claim back after a failed send (the attempt counter then decides about a retry).
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.
updateRelations()  : Result
Updates rows in a relation table.
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>
asInt()  : int
asNullableInt()  : int|null
asNullableString()  : string|null
asString()  : string
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.
rawSqlParam()  : mixed
Returns the value for a bare SQL parameter (no DBAL type).
updatePairs()  : void

Constants

FANOUT_TRIGGER_NAME

Name of the database trigger that used to do the watchdog fan-out into the queue (after insert on `*_watchdog_email_texts`).

public mixed FANOUT_TRIGGER_NAME = 'cms_mod_bazaar_watchdog_email_texts_AFTER_INSERT'

PHP self::fetchFanoutRecipients() replaces it; self::fanoutTriggerExists() uses its existence to tell when PHP may take over (see docs/sql/2026-07-06_drop_watchdog_email_fanout_triggers.sql).

DIGEST_MAX_AGE_DAYS

The digest takes pending rows only for adverts inserted within the last N days.

private int DIGEST_MAX_AGE_DAYS = 2

Properties

$entityManager

protected EntityManagerInterface $entityManager

Methods

buildFanoutSelectSql()

Builds the fan-out select (separate so it is unit-testable without the entity manager or a database).

public static buildFanoutSelectSql() : string

The structure comes from docs/sql/2026-07-03_fix_region_triggers_views_watchdog.sql (the Bazaar trigger). Type and state match like in self::insertMatchingRels(): a watchdog type or state of null means all, otherwise the advert needs a row of the parent parameter whose value is the option id (or a row of the option itself); only active watchdogs match. The original trigger compared against hard-coded parameter ids 6 and 23 from another database, so watchdogs with a type matched nothing.

Return values
string

claimSent()

Claims a pending relation for sending: `sent` 0 to 1 in one conditional UPDATE, so of two concurrent cron runs only one gets it. Returns false when the row was already claimed, sent or cancelled; the caller then skips it. A failed send gives the claim back ({@see self::releaseClaim()}).

public claimSent(int $watchdogEmailId, int $watchdogId) : bool
Parameters
$watchdogEmailId : int
$watchdogId : int
Return values
bool

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

deleteForAdverts()

Hard delete of adverts: deletes the watchdog queue for the e-mails of the given adverts before the cascade from the advert reaches `cms_mod_bazaar_watchdog_emails`.

public deleteForAdverts(array<int, int> $advertIds) : int

IMPORTANT: in Bazaar the relation to the e-mail is no action, so without this step the advert delete fails on the foreign key. Called by AdvertHardDeleteTrait inside the delete transaction.

Parameters
$advertIds : array<int, int>
Return values
int —

number of deleted queue rows

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

fanoutTriggerExists()

Tells whether the database fan-out trigger still exists.

public fanoutTriggerExists() : bool

While it does, it is authoritative and PHP fan-out holds back (in mode 3 every advert would be sent twice otherwise). After the trigger is dropped by hand (see the drop script) this returns false and PHP fan-out activates automatically, in one step with no window for a double send or an outage.

Return values
bool

fetchFanoutRecipients()

Reads the fan-out recipients, the PHP replacement of the database trigger `cms_mod_bazaar_watchdog_email_texts_AFTER_INSERT`.

public fetchFanoutRecipients(int $watchdogEmailId, array<string|int, int> $regionIds) : array<int, array{to: string|null, from: string|null, subject: string|null, body: string|null}>

Returns ready queue rows; the write is done by the table owner through EmailQueueManager::enqueueBatch().

Keeps the trigger semantics: for the given watchdog_email it takes the language texts and, per language, finds the matching watchdogs (category through the *_category_ids_in_branch view as an ancestor-or-self match, region through $regionIds, type parameter_id and state parameter_id2 against the advert parameters, see buildFanoutSelectSql()) and produces one e-mail per recipient (grouped by e-mail, from, subject and body). Only texts with a body and with the watchdog's language are used. subject and body are in the group by, otherwise MySQL would merge different languages into one row.

The foreign write into the System queue is not done here, and the join on the view over the foreign cms_system_regions is replaced by a bound IN (:regionIds). Fan-out always runs for one advert, so "the watchdog region is an ancestor-or-self of the advert region" is a constant id set computed by the owner (RegionManager::findAncestorOrSelfIds()). Empty $regionIds (an advert without a region) matches nothing.

Uses raw DBAL, because Doctrine find()/findAll() would clear the identity map. Called after the commit (completedSave), mode-aware, from WatchdogManager.

Parameters
$watchdogEmailId : int
$regionIds : array<string|int, int>

ancestors-or-self of the advert's region

Return values
array<int, array{to: string|null, from: string|null, subject: string|null, body: string|null}>

fetchPending()

Fetches pending rows for immediate sending (mode 1).

public fetchPending([int $limit = 100 ][, int $maxAttempts = 5 ]) : array<int, array{watchdog_email_id: int, watchdog_id: int, attempts: int, from: string, watchdog_email: string|null, user_id: int|null, subject: string, body: string}>

There is no join on the foreign cms_system_users. Instead of a ready recipient e-mail the method returns both own sources (watchdog_email and user_id) and the manager composes the fallback from the batch UserManager::findEmailsByIds().

NOTE: the filter is w.email IS NOT NULL OR user_id IS NOT NULL. A row whose watchdog has no e-mail and whose user has a null e-mail therefore enters the batch, uses up a slot in LIMIT and drops out in PHP. An empty string w.email = '' passes and fails at Validators::isEmail() in the manager.

Parameters
$limit : int = 100
$maxAttempts : int = 5
Return values
array<int, array{watchdog_email_id: int, watchdog_id: int, attempts: int, from: string, watchdog_email: string|null, user_id: int|null, subject: string, body: string}>

fetchPendingForDigest()

Fetches pending rows for the digest (mode 2).

public fetchPendingForDigest([int $limit = 5000 ]) : array<int, array{watchdog_email_id: int, watchdog_id: int, advert_id: int, from: string, watchdog_email: string|null, user_id: int|null, language_id: int}>

Like self::fetchPending(), there is no join on cms_system_users and the manager composes the fallback. Rows are ordered by user_id, email so rows of one recipient follow each other; the primary key completes the order, which keeps batch boundaries stable.

Pending rows are taken for adverts of the last DIGEST_MAX_AGE_DAYS days, not only today's: an advert inserted after the evening run would otherwise never be sent and its row would stay in the queue forever. The caller pages in batches until the queue is empty (a processed row is always sent or cancelled).

Parameters
$limit : int = 5000
Return values
array<int, array{watchdog_email_id: int, watchdog_id: int, advert_id: int, from: string, watchdog_email: string|null, user_id: int|null, language_id: int}>

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

incrementAttempts()

Adds one attempt; the last allowed attempt cancels the row at once.

public incrementAttempts(int $watchdogEmailId, int $watchdogId, int $maxAttempts) : void

An exhausted row would otherwise stay sent = 0, cancelled = 0 forever and retention (sent and cancelled rows only) would never delete it. cancelled comes first in set: MySQL evaluates assignments left to right and attempts must still hold the old value there (sqlite always uses old values, with the same result).

Parameters
$watchdogEmailId : int
$watchdogId : int
$maxAttempts : int

insertMatchingRels()

Inserts the relations "watchdog e-mail x matching watchdog" (modes 1 and 2, delivery is then done by the crons).

public insertMatchingRels(int $watchdogEmailId, int $advertId, array<string|int, int> $regionIds) : int

The region is matched by a bound IN (:regionIds) instead of a join on the foreign cms_system_regions. The method always runs for one advert, so "the watchdog region is an ancestor-or-self of the advert region" is a constant id set computed by the owner (RegionManager::findAncestorOrSelfIds()). Watchdogs with a null or unknown region are filtered out; empty $regionIds (an advert without a region) inserts 0 rows.

The watchdog category covers a subtree (ac.lft/rgt inside wc), so a watchdog on a parent category catches an advert in a subcategory. A watchdog type or state of null means all. An advert carries an option as a row of the parent parameter whose value is the option id (parameter_id = 4, value = '5'); the older shape, a row of the option parameter itself, stays as the second OR branch.

Parameters
$watchdogEmailId : int
$advertId : int
$regionIds : array<string|int, int>

ancestors-or-self of the advert's region

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

markCancelledBatch()

Takes undeliverable rows (a watchdog without an e-mail and a user without one) out of the pending set.

public markCancelledBatch(array<int, array{0: int, 1: int}> $pairs) : void

Skipping them without a state change would let enough of them at the head of the queue (LIMIT 100) block the immediate mode forever.

Parameters
$pairs : array<int, array{0: int, 1: int}>

markEmailRelsSent()

Marks all pending relations of one watchdog e-mail sent (the old database trigger has queued the e-mail already, {@see self::fanoutTriggerExists()}). Returns the number of marked rows.

public markEmailRelsSent(int $watchdogEmailId) : int
Parameters
$watchdogEmailId : int
Return values
int

markSentBatch()

public markSentBatch(array<int, array{0: int, 1: int}> $pairs) : void
Parameters
$pairs : array<int, array{0: int, 1: int}>

purgeProcessedOlderThan()

Applies queue retention.

public purgeProcessedOlderThan(int $days) : array{rels: int, emails: int}

Deletes sent and cancelled rows older than $days days (by insert_datetime), then the e-mails (*_watchdog_emails and texts) that no queue row points to and that are also older than $days. Pending rows are never deleted. Deletion runs in batches (BatchDelete), the rest follows on the next run.

Texts are deleted explicitly before the e-mail; the foreign key with on delete cascade is not relied on.

Parameters
$days : int
Return values
array{rels: int, emails: int}

releaseClaim()

Gives a claim back after a failed send (the attempt counter then decides about a retry).

public releaseClaim(int $watchdogEmailId, int $watchdogId) : void
Parameters
$watchdogEmailId : int
$watchdogId : int

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

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

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>

asNullableInt()

private static asNullableInt(mixed $value) : int|null
Parameters
$value : mixed
Return values
int|null

asNullableString()

private static asNullableString(mixed $value) : string|null
Parameters
$value : mixed
Return values
string|null

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

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

updatePairs()

private updatePairs(string $set, array<int, array{0: int, 1: int}> $pairs) : void
Parameters
$set : string
$pairs : array<int, array{0: int, 1: int}>
On this page

Search results