DoctrineOperationParticipant
in package
implements
OperationParticipant, EventSubscriber, UnitOfWorkResetObserver
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/unknownend 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 isend.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
MAX_REASONS
Distinct reset reasons kept per process; more are summed up under `_overflow`.
private
int
MAX_REASONS
= 32
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
$clearEvents
private
int
$clearEvents
= 0
$clearEventsOutside
private
int
$clearEventsOutside
= 0
$contractExceptions
private
int
$contractExceptions
= 0
$contractViolations
private
int
$contractViolations
= 0
$entityManager read-only
private
EntityManagerInterface
$entityManager
$logPolicy read-only
private
OperationLogPolicy
$logPolicy
= new OperationLogPolicy()
$msMax
private
float
$msMax
= 0.0
$msTotal
private
float
$msTotal
= 0.0
$operationContext read-only
private
OperationContext
$operationContext
$operations
private
int
$operations
= 0
$operationsLogged
Totals of the request summary; with `summary` most operations have no line of their own.
private
int
$operationsLogged
= 0
$outcomes
private
array<string, int>
$outcomes
= []
outcome => count
$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]
$sqlLogger read-only
private
TracingSQLLogger
$sqlLogger
$sqlMax
private
int
$sqlMax
= 0
$sqlTotal
private
int
$sqlTotal
= 0
$throwOnContractViolation read-only
private
bool|null
$throwOnContractViolation
= null
$transactionManager read-only
private
TransactionManager
$transactionManager
$unitOfWorkReset read-only
private
UnitOfWorkReset
$unitOfWorkReset
$uowSizeEndMax
private
int
$uowSizeEndMax
= 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
begin()
public
begin(OperationFrame $frame) : void
Parameters
- $frame : OperationFrame
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
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
stringonApplicationShutdown()
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
addToTotals()
private
addToTotals(OperationOutcome $outcome, float|null $ms, int|null $sql, int $uowSize) : void
Parameters
- $outcome : OperationOutcome
- $ms : float|null
- $sql : int|null
- $uowSize : int
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: listcountScheduled()
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
intformatResets()
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
stringisNoteworthy()
`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
boollog()
private
log(string $line) : void
Parameters
- $line : string
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