CoolCMS API

WorkSheetManager extends Manager
in package
uses SourceLockTransactionTrait

Work sheet manager. `WorkList` is the job (historical name), `WorkSheet` is a work sheet inside the job.

A work sheet is an invoicing batch inside a job. This manager performs the manual transitions of one sheet (closeSheet() open to closed, reopenSheet() closed|invoiced to open) and holds the read-only write guards. Bulk invoicing across the sheets of one job (open|closed to closing to invoiced) is WorkListManager::invoiceSheets(); it is an operation on the job, not on the sheet.

IMPORTANT: manual transitions run under the lock of the job, not of the sheet: GET_LOCK('company_work_list_{workListId}'), the same lock invoicing holds (WorkListManager::lockName(), one source of truth). A finer per-sheet lock would be useless: invoicing touches N sheets at once and decides by their states, so "close sheet" must wait for a concurrent "issue invoice from sheets" instead of slipping between two locks. Conditional UPDATEs (WHERE state = …) are a second, independent serialization gate: exactly one concurrent caller passes.

Write guards, four paths, each bypassing the previous one:

  • beforeSave(): insert and update; also enforces that workListId is immutable (moving a sheet between jobs would move money between clients),
  • updateColumns(): calls no hooks, and inline admin UI writes (rename, bill, order) go through it,
  • beforeDelete(): deleting a sheet with work items is rejected (see there),
  • deleteAll(): bypasses beforeDelete and is a normal Api action.

On top of all four sits the archived job guard: while the job is archived, the server rejects writes to its sheets and work items. The read-only UI mirrors this rule; it is not the only place it lives.

State transitions are not covered by the guards: they are written only by private conditional UPDATEs, which the guards do not need and so cannot be bypassed (no __closeFlow meta flag).

Tags
extends

Table of Contents

Constants

WORK_SHEETS_TABLE  : string = 'cms_mod_company_work_sheets'
Own table of the module; raw DBAL is allowed only on it (`cs-conv-model`, layer boundaries).
DELETED_IDS_META  : string = '__deletedIds'
Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
INVOICES_TABLE  : string = 'cms_mod_invoicer_invoices'
Foreign table (Invoicer): the second allowed exception to "raw DBAL only on the own table" (see {@see self::WORK_SHEETS_TABLE}), same pattern as `invoiceRowExists()` in {@see WorkListManager}. It is a read-only probe of the single column `number`: never a write, never a read of the rest of the document (that belongs to `InvoiceManager`). Used by {@see findInvoiceNumbers()}: the client job detail may show the invoice number of an invoiced sheet, while the internal `invoicerInvoiceId` is stripped ({@see \App\UI\Api\Company\Presenters\WorkItemPresenter::MY_SHEET_STRIP_FIELDS}).
LOCK_MAX_ATTEMPTS  : int = 3
LOCK_SCALE  : int = 3
LOCK_TIMEOUT  : int = 10
Seconds to wait for the job lock; same as {@see WorkListManager::LOCK_TIMEOUT}.
WORK_LISTS_TABLE  : string = 'cms_mod_company_work_lists'
Same table as {@see WorkListManager::WORK_LISTS_TABLE}; archived job guard.

Properties

$service  : Service<string|int, TEntity>
Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
$appContext  : AppContext
$assetFormatRegistry  : AssetFormatRegistry|null
Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.
$cacheTagInvalidator  : CacheTagInvalidator|null
Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
$defaultSkipFields  : array<string|int, string>
`state`, `stateBeforeClosing`, `closedDatetime` and `invoicerInvoiceId` are changed only by the transitions ({@see closeSheet()}, {@see reopenSheet()}, {@see WorkListManager::invoiceSheets()}), not by a generic `save()` from an external path; same as `WorkListManager::$defaultSkipFields`.
$internalCallContext  : InternalCallContext|null
Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).
$languageContext  : LanguageContext|null
$nonInvalidatingColumns  : array<string|int, string>
Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.
$paramsConfig  : Config|null
Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.
$presenterContext  : PresenterContext
$settingContext  : SettingContext
$totalCount  : int|null
$useTransactionForDelete  : bool
$useTransactionForSave  : bool
$em  : EntityManagerInterface
$rejectedUploads  : array<int, array{name: string, reason: string}>
New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.
$workItemManagerFactory  : WorkItemManagerFactory

Methods

__construct()  : mixed
closeSheet()  : Result
Closes a sheet manually: `open` to `closed` ("done, waiting for invoice").
delete()  : Result
Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:
deleteAll()  : Result
`deleteAll()` bypasses the hooks and is still reachable over HTTP: `Manager::deleteAll()` ({@see \App\Core\Traits\Api\Models\Managers\DeleteTrait}) maps the filter to ids and deletes them with `service->deleteAll()`, without `beforeDelete`. The guard above could be bypassed by a filter instead of ids.
find()  : T|null
findAll()  : T> : list>)
findBy()  : T|null
findInvoiceNumbers()  : array<int, string>
Returns invoice numbers for the invoiced sheets of the selection, so the client sees a readable document number. {@see \App\UI\Api\Company\Presenters\WorkItemPresenter::withSheetReferences()} emits sheet `_references` without `invoicerInvoiceId` (the internal document id, see {@see \App\UI\Api\Company\Presenters\WorkItemPresenter::MY_SHEET_STRIP_FIELDS}). The probe reads `invoicerInvoiceId` directly from the entities (no second query on `work_sheets`) and only looks up `number` in Invoicer; see {@see self::INVOICES_TABLE}.
findRelatedIds()  : array<int, int>|null
Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).
getAppContext()  : AppContext
getDefaultSkipFields()  : array<string|int, string>
Returns keys that the generic `service->save()` must not copy onto the managed entity.
getEntityClass()  : TEntity>
Returns the root Doctrine entity class managed by this manager.
getExternalMetaAllowlist()  : array<string|int, string>
Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).
getExternalOwnerData()  : array<string, mixed>
Returns `data` keys that fill the owner from the server when an external save creates a record.
getExternalRemoteIpData()  : array<string, mixed>
Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
getExternalUpdateColumnsAllowlist()  : array<string|int, string>
Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
getGraphDataKeys()  : array<string|int, string>
Returns the keys an external client may send nested in `data`.
getLanguageContext()  : LanguageContext|null
getPresenterContext()  : PresenterContext
getServiceClass()  : class-string
Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).
getSettingContext()  : SettingContext
getSettings()  : array<int, array<string, mixed>>
getTotalCount()  : int|null
initializeAllContexts()  : void
registerEvent()  : Result
reopenSheet()  : Result
Reopens a sheet: `closed | invoiced` to `open`.
save()  : Result
setAppContext()  : $this
setAssetFormatRegistry()  : static
setCacheTagInvalidator()  : static
setInternalCallContext()  : static
setLanguageContext()  : $this
setParamsConfig()  : static
setPresenterContext()  : $this
setSettingContext()  : $this
updateAllRelations()  : Result
updateColumn()  : Result
updateColumns()  : Result
Read-only guard for `updateColumns()` as well. Without it the guard in `beforeSave()` would be decoration: `updateColumns` calls no save hooks, and both the external Api action `updateColumns` and inline admin UI writes (rename, `bill`, order) go through it.
updateRelations()  : Result
acquireSourceLock()  : bool
Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).
beforeDelete()  : Result|null
IMPORTANT: delete guard with two independent conditions:
beforeSave()  : Result|null
Write guards for a sheet: 1. insert: the target job must exist and be neither deleted nor archived, 2. update: the sheet must not be locked ({@see WorkSheetState::lockedStates()}), its job must not be archived, and `workListId` must stay unchanged.
beginRejectedUploads()  : array<int, array{name: string, reason: string}>
Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
bumpCacheTags()  : void
Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
bumpCacheTagsExplicit()  : void
Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
bumpCacheTagsForClass()  : void
Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
classCacheTags()  : array<string|int, string>
Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.
connection()  : Connection
finishRejectedUploads()  : void
Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.
getDeletedId()  : int|null
Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).
getService()  : Service<string|int, TEntity>
Returns the service backing the manager traits.
lockLogChannel()  : string
readUploadedItem()  : string|null
Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.
recordRejectedUpload()  : void
Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).
releaseSourceLock()  : void
Releases the advisory lock.
runInSourceTransaction()  : T
Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).
archivedFailure()  : Result
Message of the archived job guard. Intentionally different from the locked sheet message: the way out differs (restore the job from the archive vs. reopen the sheet) and the admin must tell from it which button to use.
archivedListFailure()  : Result|null
Archived job guard. `null` means the job is open.
blockedFailure()  : Result|null
One common gate for `updateColumns`, `beforeDelete` and `deleteAll`: archived job, locked sheet and (for delete) a sheet with work items. `null` means pass.
bumpDeleteCacheTags()  : void
Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
captureDeletedIds()  : WeakMap<object, int>
Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.
columnsInvalidateCache()  : bool
deleteBatch()  : Result
Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.
errorCodeOf()  : string
failure()  : Result
fetchSheetRow()  : array<string, mixed>|null
Authoritative raw sheet row (DBAL): the guard and the transition must see the DB right now, not what the identity map or query cache holds (invoicing runs in another request and another MySQL session).
intOrNull()  : int|null
isAssetPathAllowed()  : bool
Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
ladd()  : numeric-string
lcmp()  : int
ldec()  : numeric-string
ldiv()  : numeric-string
Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).
lInt()  : int
lmul()  : numeric-string
ln()  : numeric-string
loadEntitiesForCacheTags()  : array<int, object>
Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.
lockedDeleteFailure()  : Result
Message of the delete guard. Intentionally different from the write one: "read-only" would sound like a detail for delete, yet the work items are on an issued invoice.
lockedSheetFailure()  : Result
Uniform message of the read-only guard.
lStr()  : string
lStrN()  : string|null
lsub()  : numeric-string
normalizeIds()  : array<int, int>
now()  : string
rollbackBatch()  : void
runLockedTransition()  : Result
Common envelope of manual transitions: job lock, authoritative raw read of the sheet, existence and archive guards, transition body.
saveWithHooks()  : Result
Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
unknownListStateFailure()  : Result
IMPORTANT: fail-closed guard for a raw `state` of the job (not the sheet) outside the enum, read from the same row as the archive guard. The message differs from {@see unknownStateFailure()} so the admin can tell whether the job row or the sheet row is damaged.
unknownStateFailure()  : Result
IMPORTANT: fail-closed guard for a raw `state` of the work sheet outside the enum. The message names the DB value instead of silently substituting `Open`; the column is `not null`, so only a truly damaged row gets here.
usesAssetPathTrait()  : bool
wrongStateFailure()  : Result
Failure for "this transition makes no sense from this state"; the message names what is happening to the sheet.

Constants

WORK_SHEETS_TABLE

Own table of the module; raw DBAL is allowed only on it (`cs-conv-model`, layer boundaries).

public string WORK_SHEETS_TABLE = 'cms_mod_company_work_sheets'

NOTE: two allowed exceptions to that rule: self::WORK_LISTS_TABLE (archived job guard, still the same Company module) and self::INVOICES_TABLE (foreign Invoicer module, a read-only probe of the document number).

DELETED_IDS_META

Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.

protected string DELETED_IDS_META = '__deletedIds'

Read it only through getDeletedId().

INVOICES_TABLE

Foreign table (Invoicer): the second allowed exception to "raw DBAL only on the own table" (see {@see self::WORK_SHEETS_TABLE}), same pattern as `invoiceRowExists()` in {@see WorkListManager}. It is a read-only probe of the single column `number`: never a write, never a read of the rest of the document (that belongs to `InvoiceManager`). Used by {@see findInvoiceNumbers()}: the client job detail may show the invoice number of an invoiced sheet, while the internal `invoicerInvoiceId` is stripped ({@see \App\UI\Api\Company\Presenters\WorkItemPresenter::MY_SHEET_STRIP_FIELDS}).

private string INVOICES_TABLE = 'cms_mod_invoicer_invoices'

LOCK_TIMEOUT

Seconds to wait for the job lock; same as {@see WorkListManager::LOCK_TIMEOUT}.

private int LOCK_TIMEOUT = 10

WORK_LISTS_TABLE

Same table as {@see WorkListManager::WORK_LISTS_TABLE}; archived job guard.

private string WORK_LISTS_TABLE = 'cms_mod_company_work_lists'

Properties

$service

Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).

public Service<string|int, TEntity> $service

$assetFormatRegistry

Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.

protected AssetFormatRegistry|null $assetFormatRegistry = null

Set by a decorator (decorators.neon) like setParamsConfig(): more than ten managers process assets and each has its own constructor, so constructor injection would touch all of them.

$cacheTagInvalidator

Post-commit invalidator of the wire-payload query cache (tag bump per entity class).

protected CacheTagInvalidator|null $cacheTagInvalidator = null

Set by the decorator (decorators.neon) like setParamsConfig(). Nullable and fail-open: without wiring (or without Redis) a bump is a no-op.

$defaultSkipFields

`state`, `stateBeforeClosing`, `closedDatetime` and `invoicerInvoiceId` are changed only by the transitions ({@see closeSheet()}, {@see reopenSheet()}, {@see WorkListManager::invoiceSheets()}), not by a generic `save()` from an external path; same as `WorkListManager::$defaultSkipFields`.

protected array<string|int, string> $defaultSkipFields = ['state', 'stateBeforeClosing', 'closedDatetime', 'invoicerInvoiceId']

NOTE: bill is intentionally not in the list. "Goes on the next invoice" is a plain user checkbox of the left column that the admin toggles between invoicings. It is protected by #[ExternalReadonly] (external hydration drops it) and by the locked sheet and archived job guards, not by the skip list.

$internalCallContext

Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).

protected InternalCallContext|null $internalCallContext = null

Only ApiBridge::internalCall() sets it, never the payload and not the RBAC guard, so the emergency apiRbac.internalMode: off cannot disable it. Set by a decorator like setAssetFormatRegistry(). Without it (direct manager call from cron or a test) the call counts as external.

$nonInvalidatingColumns

Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.

protected array<string|int, string> $nonInvalidatingColumns = ['views', 'viewsList', 'viewsDetail']

Covers all view counters across modules: views (Blog Article), viewsList (list impression of Advert/Message) and viewsDetail (detail view of Advert/Message, registerEvent() in the *DetailTraits). IMPORTANT: viewsDetail must stay in the list, otherwise every open of an advert/post detail would invalidate all lists of the class.

$paramsConfig

Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.

protected Config|null $paramsConfig = null

Write logic (asset directories) reads paths from here, not from getSettings() or the setting context, which may be filled by the client without server paths. The property cannot be named $config: it collides with the promoted $config in some managers.

$useTransactionForDelete

protected bool $useTransactionForDelete = true

$useTransactionForSave

protected bool $useTransactionForSave = true

$rejectedUploads

New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.

private array<int, array{name: string, reason: string}> $rejectedUploads = []

Methods

closeSheet()

Closes a sheet manually: `open` to `closed` ("done, waiting for invoice").

public closeSheet(int $workSheetId) : Result

It does not invoice and does not touch the document. It is only a signal for staff (and for the UI group "closed, waiting for invoice") that also locks the sheet against further writes.

Idempotence is a property of the conditional UPDATE (WHERE state = 'open'): a second call does nothing and returns a readable failure, not a silent no-op. The state is read raw beforehand so the message can say what happened (already closed, being invoiced, or invoiced).

Parameters
$workSheetId : int
Return values
Result —

success data array{workSheetId: int, workListId: int, state: string}

delete()

Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:

public delete(int|array<string|int, mixed> $id[, array<string, mixed> $meta = [] ]) : Result
  • beforeDelete(array $entities, array $meta): ?Result runs before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows into afterDelete under $meta['__beforeDelete'].
  • afterDelete(array $entities, Result $result, array $meta): ?Result runs after the delete, still in the transaction. It is only for recomputing derived state from the remaining rows (toped_until, member_until, county), idempotent and reversible. A failure rolls back the delete itself (atomicity). It is also the place for the mandatory delete of dependents through a foreign manager (comment, rating and message threads): an owner still managed in the unit of work references them, so they must go after its row, never before it in beforeDelete (collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only into completedDelete.
  • completedDelete(array $entities, Result $result, array $meta): void runs best effort after the commit, outside the transaction; an exception is only logged (the delete stays).
  • completedAssetCleanup(array $entities, Result $result, array $meta): void runs best effort after the commit and releases the Asset rows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in is ownedAssetIds(object $entity): array on the manager, without it the hook does not run.

IMPORTANT: the cleanup has its own name instead of completedDelete() because many Api managers define completedDelete() themselves and a class method overrides a trait method without a warning, so the cleanup would silently be missing in them.

IMPORTANT: a soft delete (deleted = 1) goes through save(), not here; assets must not be released there because the record can be restored.

$entities are the entities loaded before the delete (after the DELETE the row is gone, but the PHP object keeps the data, such as FKs for the recomputation).

IMPORTANT: the id is the exception. Doctrine nulls the id of a removed instance on flush(), so a hook must not rely on getId() once the delete ran (today only the read UoW::clear() in the service find() hands the preloaded entities over detached and keeps their id). Every hook therefore gets the ids captured before beforeDelete under the reserved key DELETED_IDS_META, keyed by the loaded entity object (an input id that loaded nothing has no entry); read it through getDeletedId(). The key is always set here, so a caller-supplied value never survives.

Without hooks it behaves exactly as before (no preloading, no transaction). NOTE: deleteAll() (bulk) and the tree deleteNode take another path and do not trigger these hooks.

Parameters
$id : int|array<string|int, mixed>
$meta : array<string, mixed> = []
Return values
Result

deleteAll()

`deleteAll()` bypasses the hooks and is still reachable over HTTP: `Manager::deleteAll()` ({@see \App\Core\Traits\Api\Models\Managers\DeleteTrait}) maps the filter to ids and deletes them with `service->deleteAll()`, without `beforeDelete`. The guard above could be bypassed by a filter instead of ids.

public deleteAll([array<int|string, mixed> $filters = [] ]) : Result

The affected set is therefore determined by the same filter and run through the same test. Behavior for unaffected sets (no locked or non-empty sheet in the selection) stays 1:1 with the parent.

Parameters
$filters : array<int|string, mixed> = []
Return values
Result

find()

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

findAll()

public findAll([array<string|int, mixed>|null $includes = [] ][, 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 ]) : T> : list>)
Parameters
$includes : array<string|int, mixed>|null = []
$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
Return values
T> : list>) —

entities, or raw rows when $columns is set

findBy()

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

findInvoiceNumbers()

Returns invoice numbers for the invoiced sheets of the selection, so the client sees a readable document number. {@see \App\UI\Api\Company\Presenters\WorkItemPresenter::withSheetReferences()} emits sheet `_references` without `invoicerInvoiceId` (the internal document id, see {@see \App\UI\Api\Company\Presenters\WorkItemPresenter::MY_SHEET_STRIP_FIELDS}). The probe reads `invoicerInvoiceId` directly from the entities (no second query on `work_sheets`) and only looks up `number` in Invoicer; see {@see self::INVOICES_TABLE}.

public findInvoiceNumbers(array<int, WorkSheet> $sheets) : array<int, string>

No call, no sheet with a document, or a foreign/deleted document in Invoicer gives [] or an omitted key, never an error (the client detail must not fail over a missing invoice number).

Parameters
$sheets : array<int, WorkSheet>
Return values
array<int, string> —

workSheetId => invoice number; only sheets that have a document and whose document still exists in Invoicer

findRelatedIds()

Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).

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 —

null when it cannot be determined (collection mapping missing / database error)

getDefaultSkipFields()

Returns keys that the generic `service->save()` must not copy onto the managed entity.

public getDefaultSkipFields() : array<string|int, string>

The manager's own afterSave()/beforeSave() sync trait handles them through $meta, not through the object graph. Backed by an optional protected per-manager property. The write Api endpoint (SaveTrait::internalActionSave()) also uses this accessor to strip the same keys from $data before hydration, because defaultSkipFields alone does not protect hydration.

Return values
array<string|int, string>

getEntityClass()

Returns the root Doctrine entity class managed by this manager.

public getEntityClass() : TEntity>

The write Api endpoint (SaveTrait) takes it from here instead of the request, so a client cannot choose the instantiated PHP class.

Return values
TEntity>

getExternalMetaAllowlist()

Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).

public getExternalMetaAllowlist() : array<string|int, string>

The internal path (ApiBridge::internalCall() calling internalActionSave() directly, admin and front) is not limited by this allowlist and reads $meta unchanged. The default is an empty array, so external meta is dropped entirely; actionSave() is reachable through HTTP routing regardless of ApiBridge, hence the strip. Override per manager with the protected property $externalMetaAllowlist, analogous to self::getDefaultSkipFields().

Return values
array<string|int, string>

getExternalOwnerData()

Returns `data` keys that fill the owner from the server when an external save creates a record.

public getExternalOwnerData(int $userId) : array<string, mixed>

The default [] means the domain has no owner or must not be created through external save (Order uses the dedicated checkout action, User registers another way). On INSERT there is nothing to own, so the owner cannot be verified, only determined: the owner field is #[ExternalReadonly] (the strip removes it from the payload) and SaveTrait::applyExternalOwner() then fills it from the JWT, so the client value never reaches hydration.

IMPORTANT: return the key that matches the column mapping, otherwise the owner is silently not written:

  • Watchdog/Comment/Rating/Message: scalar column, ['userId' => $userId];
  • Advert (Bazaar/Comcat): mapped column, ['insertUserId' => $userId];
  • Bazaar Response/AdvertFollower: the scalar userId has an #[ORM\AttributeOverride] with insertable: false, updatable: false in the Api leaf and the column belongs to the association $user, so ['user' => ['id' => $userId]]; Comcat Response has no $user association, so there it is ['userId' => $userId];
  • Complaint: the owner is the association $user, ['user' => ['id' => $userId]].

['id' => X] is a reference that hydrateForPersist() looks up in the DB (OrmEntityHydrator::isReference()). Called only on the external path and only on INSERT; on UPDATE it would make whoever saves a foreign record its owner.

Parameters
$userId : int

identity from the JWT (never from the request)

Return values
array<string, mixed>

getExternalRemoteIpData()

Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.

public getExternalRemoteIpData(string $ip) : array<string, mixed>

The IP field is #[ExternalReadonly], so the external strip removes it and the client cannot forge it. Without filling, an external insert would have no IP and the "anonymous by IP" deduplication and per-IP manager limits would silently stop working. SaveTrait::applyExternalRemoteIp() fills it from IRequest::getRemoteAddress(), never from the request. The default [] means the entity carries no IP; overrides return the entity field name (['insertIp' => $ip]).

Parameters
$ip : string
Return values
array<string, mixed>

getExternalUpdateColumnsAllowlist()

Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.

public getExternalUpdateColumnsAllowlist() : array<string|int, string>

updateColumn on a to-many relation is DELETE-all plus INSERT, so without this filter an external POST with values={userRoles: [...]} would bypass the strip/guard of the save path (privilege escalation). The internal path (ApiBridge::internalCall() calling internalActionUpdateColumns() directly) is not limited. The default is an empty array, so external updateColumns is rejected entirely. Override with the protected property $externalUpdateColumnsAllowlist.

Return values
array<string|int, string>

getGraphDataKeys()

Returns the keys an external client may send nested in `data`.

public getGraphDataKeys() : array<string|int, string>

Before hydration the write Api endpoint (SaveTrait::internalActionSave()) moves them from $data to $meta, where the manager's own afterSave()/beforeSave() sync trait reads them (the same mechanism as internal meta). When the client also sends the same key explicitly in meta, meta wins (compatibility with admin, e.g. the categoryRels collision on Advert). Backed by an optional protected per-manager property, like self::getDefaultSkipFields().

IMPORTANT: promoting a key here does not make its content safe to write as received. When the key carries a sensitive field that the server otherwise dictates (typically a payment or approval flag), the manager must overwrite or verify it server-side on the external path in beforeSave()/afterSave() before its sync trait reads it; otherwise the client sets it itself (privilege escalation). The external path is recognized by $meta['__external'] === true, which only SaveTrait::actionSave() sets (never internalActionSave() alone), so internal calls never get the marker. Example: Bazaar Advert.categoryRels is persisted through the object graph (owned collection outside $graphDataKeys); a client could mark paid itself and get a paid advert without payment, so AdvertManager::beforeSave() overwrites it through CategoryRelsPaidSanitizationTrait::sanitizeExternalCategoryRelsPaid() before flush.

Return values
array<string|int, string>

getServiceClass()

Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).

public getServiceClass() : class-string

The cache is controlled at the Api presenter boundary through this identity.

Return values
class-string

getSettings()

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

getTotalCount()

public getTotalCount() : int|null
Return values
int|null

registerEvent()

public registerEvent(array<string|int, int> $ids, class-string $entityClass, string $property) : Result
Parameters
$ids : array<string|int, int>
$entityClass : class-string

entity used to translate the property into a column in the expression

$property : string
Return values
Result

reopenSheet()

Reopens a sheet: `closed | invoiced` to `open`.

public reopenSheet(int $workSheetId) : Result

IMPORTANT: the invoice stays. invoicer_invoice_id is not cleared. The document is issued, has a number from the live series and is at the client; "un-invoicing" it by deleting the link would turn the records into a lie and the invoice into an orphan. A reopened sheet is still the one the invoice #X came from, it just accepts writes again; an additional charge is issued as another invoice from the same (or a following) sheet.

bill is not set to 1: invoicing reset it to 0 after issuing the document, and whether the sheet goes on the next invoice again is the staff's decision, not a side effect of reopening. closed_datetime and state_before_closing are cleared; the sheet runs again and has no interrupted transition behind it.

closing is not reopened: it is not a "closed sheet" but a running invoicing (or its leftover after a crash), and touching it by a manual transition is exactly the path to a second invoice that the whole design avoids. The recovery in invoiceSheets() handles it.

Parameters
$workSheetId : int
Return values
Result —

success data array{workSheetId: int, workListId: int, state: string, invoiceId: ?int}

save()

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

setParamsConfig()

public setParamsConfig(Config $paramsConfig) : static
Parameters
$paramsConfig : Config
Return values
static

updateAllRelations()

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

id, or a list of ids / composite-id arrays

$property : string
$values : array<string, mixed>

relation property name => value set on every row

Return values
Result

updateColumn()

public updateColumn(int|array<string|int, mixed> $entityId, string $property, mixed $value) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$value : mixed
Return values
Result

updateColumns()

Read-only guard for `updateColumns()` as well. Without it the guard in `beforeSave()` would be decoration: `updateColumns` calls no save hooks, and both the external Api action `updateColumns` and inline admin UI writes (rename, `bill`, order) go through it.

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

workListId must not change here for the same reason as in beforeSave().

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

updateRelations()

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

id, or a list of ids / composite-id arrays

$property : string
$relationEntities : array<string|int, object>

relation entities with updated values

Return values
Result

acquireSourceLock()

Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).

protected acquireSourceLock(string $name, int $timeout) : bool
Parameters
$name : string
$timeout : int
Return values
bool

beforeDelete()

IMPORTANT: delete guard with two independent conditions:

protected beforeDelete(array<int, WorkSheet> $entities[, array<string, mixed> $meta = [] ]) : Result|null
  1. a locked sheet is not deleted. An invoiced sheet holds the link to the document and its work items are on an issued invoice; deleting it would remove hours somebody was invoiced for from the records.
  2. a sheet with non-deleted work items is not deleted. The FK fk_company_work_item_sheet is ON DELETE RESTRICT, so a hard delete would end in an unreadable SQL error; a soft delete (deleted = 1) would not be stopped by the restriction at all and orphan work items would stay under a sheet the UI never shows, still counted in the job totals. The guard enforces "work items first, then the sheet" and tells the admin how many items remain.

State and counts are read raw from the DB, not from the passed entities, which may come from an identity map from before invoicing. Covers delete(id) and delete([ids]): DeleteTrait runs the hooks for arrays and rolls back the whole batch when beforeDelete fails (atomicity over partial cleanup).

Parameters
$entities : array<int, WorkSheet>
$meta : array<string, mixed> = []
Return values
Result|null

beforeSave()

Write guards for a sheet: 1. insert: the target job must exist and be neither deleted nor archived, 2. update: the sheet must not be locked ({@see WorkSheetState::lockedStates()}), its job must not be archived, and `workListId` must stay unchanged.

protected beforeSave(bool $isInsert, WorkSheet &$entity[, array<string, mixed> &$meta = [] ]) : Result|null

Why workListId is immutable: the client, the accounting customer and the client account all sit on the job. Moving a sheet between jobs would hand all its work items (and their private conversations) to another client through one inconspicuous form field. There is no legitimate reason: the job is created before the sheet.

The comparison is against a raw SELECT, not the entity value: that came from payload hydration, so it is the caller's wish, not the DB state (same reason as the guard in WorkItemManager::beforeSave()).

Parameters
$isInsert : bool
$entity : WorkSheet
$meta : array<string, mixed> = []
Return values
Result|null

beginRejectedUploads()

Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.

protected beginRejectedUploads() : array<int, array{name: string, reason: string}>
Return values
array<int, array{name: string, reason: string}> —

the outer save's list

bumpCacheTags()

Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.

protected bumpCacheTags(object $entity) : void

Fail-open (the invalidator swallows exceptions). Called from the manager Save/Delete/Tree traits after the commit.

Parameters
$entity : object

bumpCacheTagsExplicit()

Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).

protected bumpCacheTagsExplicit(array<string|int, string> $classes) : void
Parameters
$classes : array<string|int, string>

bumpCacheTagsForClass()

Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).

protected bumpCacheTagsForClass(class-string $rootClass[, array<string|int, string> $extraTags = [] ]) : void

Bumps the root class and its owned-cascade classes.

Parameters
$rootClass : class-string
$extraTags : array<string|int, string> = []

classCacheTags()

Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.

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

IMPORTANT: additionalCacheTags(object) receives an entity, so a class-level bump cannot use it; without this hook a delete would lose the cross-entity tag (Comment to CommentThread). A manager whose cross-entity tag does not depend on a particular entity returns it here.

NOTE: unlike additionalCacheTags(), this hook is declared (with an empty default) instead of being an opt-in through method_exists(); method_exists() is always true in a manager that has the method, which phpstan reports as alreadyNarrowedType.

Return values
array<string|int, string>

connection()

protected connection() : Connection
Return values
Connection

finishRejectedUploads()

Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.

protected finishRejectedUploads(Result|null $result, array<int, array{name: string, reason: string}> $outer) : void
Parameters
$result : Result|null
$outer : array<int, array{name: string, reason: string}>

list from beginRejectedUploads()

getDeletedId()

Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).

protected getDeletedId(array<string, mixed> $meta, object $entity) : int|null
Parameters
$meta : array<string, mixed>
$entity : object
Return values
int|null

getService()

Returns the service backing the manager traits.

protected getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>

lockLogChannel()

protected lockLogChannel() : string
Return values
string

readUploadedItem()

Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.

protected readUploadedItem(array<string|int, mixed> $item[, bool $strictBase64 = true ][, int|null $maxBytes = null ]) : string|null

Call it right before processing and release the result before the next item, so that files never meet in memory. A path is accepted only during an internal bridge call ($internalCallContext) and only inside the dropzone root. A rejected path is logged to the auth channel and null is returned; callers treat it like unreadable data (skip the item or refuse the save).

Parameters
$item : array<string|int, mixed>
$strictBase64 : bool = true

false for callers that decode loosely (base64_decode($x))

$maxBytes : int|null = null

size limit; for path it is checked before reading

Return values
string|null

recordRejectedUpload()

Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).

protected recordRejectedUpload(mixed $name, string $reason) : void

Called by the places that drop an item (processOrderedAssets(), advert attachments, product set gallery).

Parameters
$name : mixed
$reason : string

releaseSourceLock()

Releases the advisory lock.

protected releaseSourceLock(string $name) : void

IMPORTANT: it is called from a finally, so a failure here (a gone-away connection after a fatal query) is swallowed and logged; rethrowing would mask the original exception being unwound. MySQL drops session locks when the connection dies anyway, so nothing leaks past the process.

Parameters
$name : string

runInSourceTransaction()

Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).

protected runInSourceTransaction(callable(Connection): T $fn) : T

The advisory lock (held by the caller across this call) is not released by a rollback, so every attempt runs under the same serialization.

Parameters
$fn : callable(Connection): T
Tags
template
Return values
T

archivedFailure()

Message of the archived job guard. Intentionally different from the locked sheet message: the way out differs (restore the job from the archive vs. reopen the sheet) and the admin must tell from it which button to use.

private static archivedFailure() : Result
Return values
Result

archivedListFailure()

Archived job guard. `null` means the job is open.

private archivedListFailure(int $workListId[, bool $requireExists = false ]) : Result|null
Parameters
$workListId : int
$requireExists : bool = false

on insert the job must exist and be non-deleted; a sheet without a job is an orphan (the FK would reject it too, just unreadably)

Return values
Result|null

blockedFailure()

One common gate for `updateColumns`, `beforeDelete` and `deleteAll`: archived job, locked sheet and (for delete) a sheet with work items. `null` means pass.

private blockedFailure(array<int, int> $sheetIds[, bool $forDelete = false ]) : Result|null
Parameters
$sheetIds : array<int, int>
$forDelete : bool = false

adds the check "the sheet must not have non-deleted work items" and uses the delete messages (intentionally different from the write ones: this is a cascade, not a detail)

Return values
Result|null

bumpDeleteCacheTags()

Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.

private bumpDeleteCacheTags(array<int, object> $entities) : void
Parameters
$entities : array<int, object>

captureDeletedIds()

Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.

private captureDeletedIds(array<int, object> $entities) : WeakMap<object, int>
Parameters
$entities : array<int, object>
Return values
WeakMap<object, int>

columnsInvalidateCache()

private columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
$columns : array<int, string>
Return values
bool

deleteBatch()

Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.

private deleteBatch(array<int, int|string|array<string|int, mixed>> $ids) : Result

Transaction ownership: the wrapper only opens its own level; inside a caller's transaction a rollback here marks the caller's transaction rollback-only, so its final commit rolls the whole scope back (TransactionManager counter contract, the same as the hooked delete() path).

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

fetchSheetRow()

Authoritative raw sheet row (DBAL): the guard and the transition must see the DB right now, not what the identity map or query cache holds (invoicing runs in another request and another MySQL session).

private fetchSheetRow(int $workSheetId) : array<string, mixed>|null
Parameters
$workSheetId : int
Return values
array<string, mixed>|null

intOrNull()

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

isAssetPathAllowed()

Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.

private isAssetPathAllowed(BaseEntity $entity) : bool
Parameters
$entity : BaseEntity
Return values
bool

ladd()

private ladd(string $a, string $b) : numeric-string
Parameters
$a : string
$b : string
Return values
numeric-string

ldiv()

Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).

private ldiv(string $a, string $b) : numeric-string

IMPORTANT: truncation would systematically drop up to 0.001 from every unit on large quantities, so the invariant sum(unitPrice x quantity) ~ sum(rowBase) would drift in one direction (an issue document undervalued against the order).

Parameters
$a : string
$b : string
Return values
numeric-string

lmul()

private lmul(string $a, string $b) : numeric-string
Parameters
$a : string
$b : string
Return values
numeric-string

loadEntitiesForCacheTags()

Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.

private loadEntitiesForCacheTags(int|array<string|int, mixed> $id) : array<int, object>
Parameters
$id : int|array<string|int, mixed>
Return values
array<int, object>

lockedDeleteFailure()

Message of the delete guard. Intentionally different from the write one: "read-only" would sound like a detail for delete, yet the work items are on an issued invoice.

private lockedDeleteFailure(array<int, int> $sheetIds, WorkSheetState $state) : Result
Parameters
$sheetIds : array<int, int>
$state : WorkSheetState
Return values
Result

lsub()

private lsub(string $a, string $b) : numeric-string
Parameters
$a : string
$b : string
Return values
numeric-string

normalizeIds()

private normalizeIds(array<int, int> $ids) : array<int, int>
Parameters
$ids : array<int, int>
Return values
array<int, int>

runLockedTransition()

Common envelope of manual transitions: job lock, authoritative raw read of the sheet, existence and archive guards, transition body.

private runLockedTransition(int $workSheetId, callable(int, int, WorkSheetState, Array): Result $transition) : Result

The row is read twice: once before the lock (for work_list_id, without which there is nothing to lock) and once under the lock (authoritative; anyone may have changed the state meanwhile). work_list_id is immutable (beforeSave()), so the lock cannot miss the target.

Parameters
$workSheetId : int
$transition : callable(int, int, WorkSheetState, Array): Result
Return values
Result

saveWithHooks()

Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.

private saveWithHooks(TEntity $entity, array<string, mixed> $meta, array<int, string> $skipFields) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed>
$skipFields : array<int, string>
Return values
Result

unknownListStateFailure()

IMPORTANT: fail-closed guard for a raw `state` of the job (not the sheet) outside the enum, read from the same row as the archive guard. The message differs from {@see unknownStateFailure()} so the admin can tell whether the job row or the sheet row is damaged.

private unknownListStateFailure(int $workListId, string $raw) : Result
Parameters
$workListId : int
$raw : string
Return values
Result

unknownStateFailure()

IMPORTANT: fail-closed guard for a raw `state` of the work sheet outside the enum. The message names the DB value instead of silently substituting `Open`; the column is `not null`, so only a truly damaged row gets here.

private unknownStateFailure(array<int, int> $sheetIds, string $raw) : Result
Parameters
$sheetIds : array<int, int>
$raw : string
Return values
Result

usesAssetPathTrait()

private static usesAssetPathTrait(string $class) : bool
Parameters
$class : string
Return values
bool

wrongStateFailure()

Failure for "this transition makes no sense from this state"; the message names what is happening to the sheet.

private wrongStateFailure(WorkSheetState $state, string $action) : Result
Parameters
$state : WorkSheetState
$action : string
Return values
Result
On this page

Search results