CoolCMS API

DoctrineOperationParticipant
in package
implements OperationParticipant, EventSubscriber, UnitOfWorkResetObserver

FinalYes

Owns the unit of work of the default EntityManager around each root operation and logs it.

For the log it reads cheap counters only: the size of the identity map, the number of scheduled changes, whether the EM is open, the transaction levels and the SQL query counter. It never walks entities or their properties (lazyNativeObjects: true would initialize them), runs no SQL, never flushes, and logs scalars only; the only things it changes are the boundary clears below and the rollback of levels an operation left open.

Doctrine publishes onClear only after the unit of work was emptied, so the listener cannot count detached entities; it counts clear events. That is why the log line has two different numbers: clear_events (how many clears ran during the operation) and uow_size_end (what is managed when the operation ends). Neither is a peak.

As the UnitOfWorkResetObserver of UnitOfWorkReset it also counts the resets by kind and reason (clear_abort, clear_batch, clear_reset, clear_recover, detach, reset_reasons); clear_other is what that service does not explain.

Clears outside any operation (for example the leftover-transaction rollback on Application::onShutdown, after the response was sent) are summed up and written in the request summary, registered on onShutdown after that safety net.

Reads never clear the unit of work; this participant does, at the boundaries of a root operation (joined frames never reach it):

  • begin: clears through UnitOfWorkReset::beginOperation() only when the EM is open, no DBAL transaction is active and nothing is scheduled; otherwise it clears nothing and records a contract violation (begin.em_closed, begin.transaction_active, begin.pending_work);
  • end: clears through UnitOfWorkReset::endOperation() only when the EM is open and no DBAL transaction is active (so a failed operation inside an open transaction is left to whoever rolls it back). Scheduled work left at a succeeded/unknown end is a violation (end.pending_work): it is logged and counted, never flushed; the clear then drops it. A transaction opened by the operation and left open is end.transaction_left_open. Scheduled updates exist only during a flush, so a dirty managed entity that was never flushed is not detected.

With throwing on (params.1.operations.contractViolation: auto = outside Tracy production mode, throw, log) a violation from self::THROWING_VIOLATIONS ends the operation with OperationContractViolationException, never over the operation's own exception. Before throwing, the participant cleans up: the transaction levels the operation opened and left open are rolled back down to where the operation began, then the end clear runs as usual. The log line is written first; the scope rethrows only after the frame is closed. A transaction of the caller stays the caller's: it gets the exception and its rollback discards the rest.

Log volume (params.1.operations.log, OperationLogPolicy): verbose writes every operation, summary only those worth a look (self::isNoteworthy()); the request summary always carries the totals of all of them.

Table of Contents

Interfaces

OperationParticipant
Store that wants to know where an operation begins and ends (Doctrine, later cache or search).
EventSubscriber
UnitOfWorkResetObserver
Gets told about every reset that {@see UnitOfWorkReset} has just carried out.

Constants

MAX_REASONS  : int = 32
Distinct reset reasons kept per process; more are summed up under `_overflow`.
THROWING_VIOLATIONS  : array<string|int, mixed> = ['begin.pending_work', 'end.pending_work', 'end...
Violations that are a bug of the operation (or of the code right before it), so they throw when throwing is on. Deliberately not here: - `begin.transaction_active`: an operation called inside the caller's transaction is legitimate (integration tests wrap operations in their own transaction; a nested scenario runs inside the save of its caller). The participant only stands aside and leaves the unit of work to the transaction owner, which commits or rolls back (and clears) it.

Properties

$begins  : WeakMap<OperationFrame, array{time: int|float, sql: int, clears: int, resets: array, resetReasons: array, uowSize: int, txLevel: int, dbalLevel: int, violations: list}>
$clearEvents  : int
$clearEventsOutside  : int
$contractExceptions  : int
$contractViolations  : int
$entityManager  : EntityManagerInterface
$logPolicy  : OperationLogPolicy
$msMax  : float
$msTotal  : float
$operationContext  : OperationContext
$operations  : int
$operationsLogged  : int
Totals of the request summary; with `summary` most operations have no line of their own.
$outcomes  : array<string, int>
$resetReasons  : array<string, int>
$resets  : array<string, int>
Process-wide sums of the resets done through {@see UnitOfWorkReset}, by kind.
$sqlLogger  : TracingSQLLogger
$sqlMax  : int
$sqlTotal  : int
$throwOnContractViolation  : bool|null
$transactionManager  : TransactionManager
$unitOfWorkReset  : UnitOfWorkReset
$uowSizeEndMax  : int

Methods

__construct()  : mixed
begin()  : void
end()  : void
getSubscribedEvents()  : array<int, string>
Constant list: Nettrine reads it on an instance created without the constructor.
key()  : string
Stable name of the resource, e.g. `doctrine.default`; unique among participants.
onApplicationShutdown()  : void
Writes the request summary on `Application::onShutdown`, registered after `TransactionManager::closeLeftoverTransaction()`, so its rollback and clear are included.
onClear()  : void
Counts one clear of the unit of work; O(1), no SQL, no log line, never throws.
recordReset()  : void
Counts one reset done through {@see UnitOfWorkReset}; O(1), scalars only, never throws.
addToTotals()  : void
computeResets()  : array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}
Reset counters since a snapshot.
countScheduled()  : int
Scheduled changes only; a dirty entity that was never flushed has none (see the class docblock).
formatResets()  : string
isNoteworthy()  : bool
`summary` level: an operation gets its own line only when something about it is worth a look.
log()  : void
rollbackOpenedTransactions()  : void
Rolls back the transaction levels the operation opened and left open, down to the levels it began with: first those of {@see TransactionManager} (the outermost one rolls back physically and clears), then plain DBAL levels opened beside it. The caller's own levels stay. A failure is logged; the contract exception follows anyway.

Constants

THROWING_VIOLATIONS

Violations that are a bug of the operation (or of the code right before it), so they throw when throwing is on. Deliberately not here: - `begin.transaction_active`: an operation called inside the caller's transaction is legitimate (integration tests wrap operations in their own transaction; a nested scenario runs inside the save of its caller). The participant only stands aside and leaves the unit of work to the transaction owner, which commits or rolls back (and clears) it.

private array<string|int, mixed> THROWING_VIOLATIONS = ['begin.pending_work', 'end.pending_work', 'end.transaction_left_open']
  • begin.em_closed: left by an earlier failure that already reported its own exception; writes of this operation fail on the closed EM with theirs.

Properties

$begins

private WeakMap<OperationFrame, array{time: int|float, sql: int, clears: int, resets: array, resetReasons: array, uowSize: int, txLevel: int, dbalLevel: int, violations: list}> $begins

$operationsLogged

Totals of the request summary; with `summary` most operations have no line of their own.

private int $operationsLogged = 0

$resetReasons

private array<string, int> $resetReasons = []

kind/reason => count, at most self::MAX_REASONS keys + _overflow

$resets

Process-wide sums of the resets done through {@see UnitOfWorkReset}, by kind.

private array<string, int> $resets = [\App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_ABORT => 0, \App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_BATCH => 0, \App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_RESET => 0, \App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_RECOVER => 0, \App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_DETACH => 0, \App\Core\Doctrine\Operations\UnitOfWorkReset::KIND_OPERATION => 0]

Methods

__construct()

public __construct(EntityManagerInterface $entityManager, TransactionManager $transactionManager, OperationContext $operationContext, TracingSQLLogger $sqlLogger[, OperationLogPolicy $logPolicy = new OperationLogPolicy() ][, bool|null $throwOnContractViolation = null ]) : mixed
Parameters
$entityManager : EntityManagerInterface
$transactionManager : TransactionManager
$operationContext : OperationContext
$sqlLogger : TracingSQLLogger
$logPolicy : OperationLogPolicy = new OperationLogPolicy()
$throwOnContractViolation : bool|null = null

end()

public end(OperationFrame $frame, OperationOutcome $outcome, Throwable|null $primary) : void
Parameters
$frame : OperationFrame
$outcome : OperationOutcome
$primary : Throwable|null

the exception that leaves the operation; it is rethrown by the caller after this call, so a participant must not keep it

Tags
throws
OperationContractViolationException

throwing on, a violation of self::THROWING_VIOLATIONS and no primary exception; thrown after the cleanup

getSubscribedEvents()

Constant list: Nettrine reads it on an instance created without the constructor.

public getSubscribedEvents() : array<int, string>
Return values
array<int, string>

key()

Stable name of the resource, e.g. `doctrine.default`; unique among participants.

public key() : string
Return values
string

onApplicationShutdown()

Writes the request summary on `Application::onShutdown`, registered after `TransactionManager::closeLeftoverTransaction()`, so its rollback and clear are included.

public onApplicationShutdown(mixed ...$shutdownArgs) : void

The response is already sent at this point.

Parameters
$shutdownArgs : mixed

onClear()

Counts one clear of the unit of work; O(1), no SQL, no log line, never throws.

public onClear() : void

IMPORTANT: Nettrine's event manager calls listeners without a try/catch, and the event is raised from inside UnitOfWork::clear() on the read path. An exception here would fail the read after the identity map is already gone.

recordReset()

Counts one reset done through {@see UnitOfWorkReset}; O(1), scalars only, never throws.

public recordReset(string $kind, string $reason, int $count) : void

The per-operation numbers are differences against the snapshot taken in begin().

Parameters
$kind : string

one of the UnitOfWorkReset::KIND_* constants

$reason : string

normalized reason given by the caller ([a-z0-9._-], at most 64 chars)

$count : int

1 for a clear; the number of detached entities for a discard

computeResets()

Reset counters since a snapshot.

private computeResets(int $clears, array<string, int> $resetsBefore, array<string, int> $reasonsBefore) : array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}

other = clears that are not a reset through UnitOfWorkReset: rollback in TransactionManager, EntityManager::close() after a failed flush, the hydrator without the service. A negative value means the listener missed a clear.

Parameters
$clears : int
$resetsBefore : array<string, int>
$reasonsBefore : array<string, int>
Return values
array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}

countScheduled()

Scheduled changes only; a dirty entity that was never flushed has none (see the class docblock).

private static countScheduled(UnitOfWork $unitOfWork) : int
Parameters
$unitOfWork : UnitOfWork
Return values
int

formatResets()

private static formatResets(array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}|null $resets) : string
Parameters
$resets : array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}|null
Return values
string

isNoteworthy()

`summary` level: an operation gets its own line only when something about it is worth a look.

private isNoteworthy(OperationOutcome $outcome, array<int, string> $violations, float|null $ms, int|null $sql, int $uowSize, array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}|null $resets) : bool

Routine batch clears and the operation boundary clears are not; an operation without a begin snapshot (no numbers) always is.

Parameters
$outcome : OperationOutcome
$violations : array<int, string>
$ms : float|null
$sql : int|null
$uowSize : int
$resets : array{abort: int, batch: int, reset: int, recover: int, operation: int, other: int, detach: int, reasons: list}|null
Return values
bool

rollbackOpenedTransactions()

Rolls back the transaction levels the operation opened and left open, down to the levels it began with: first those of {@see TransactionManager} (the outermost one rolls back physically and clears), then plain DBAL levels opened beside it. The caller's own levels stay. A failure is logged; the contract exception follows anyway.

private rollbackOpenedTransactions(int $txLevelBegin, int $dbalLevelBegin) : void
Parameters
$txLevelBegin : int
$dbalLevelBegin : int
On this page

Search results