OrderManager
extends Manager
in package
uses
BlacklistGuardTrait, OrderBuildTrait, OrderCronTrait
Order manager: owns the order model and its calculation.
State transitions (invoice, e-mails, vouchers, points) stay behind the single entry point
OrderTransitionManager and do not belong here. Stock reservation is delegated to
OrderStockManager.
beforeSave() (inside the save transaction):
- Insert: fills
insertDatetime, generatesorderNumberand the variable symbol via OrderNumberGenerator, and calls VoucherManager::incrementUsage() for every voucher with a live FK. An exhausted voucher rejects the whole save atomically and the rejection callsem->clear(). - Update: rejects an order without
itemswhen the DB has items, because the managed copy would delete all items, units and fees through orphan removal and the totals would be recalculated to 0 (useupdateColumn()for partial updates). Stores a snapshot oforder_item_unitsin$meta['_unitsBefore']and diffs the voucher FKs against the DB state, incrementing added and decrementing removed vouchers. The voucher diff is skipped when the DB hasis_cancelled=1(read by raw SQL, the entity may be stale): the counters of a canceled order belong to OrderTransitionManager, otherwise a later restore would increment twice. - Always: authoritative recalculation through OrderCalculator::recalculate(); stored totals are not trusted.
completedSave() (post-commit, best-effort): insert calls OrderStockManager::reserve(),
an update with changed units calls rereserve(), unchanged units do not touch the stock.
$meta['allowPartial'] overrides the default, $meta['skipReserve']=true skips the stock
and the order document refresh. A stock failure does not fail the save (the order is valid
unreserved, see stock_state); it only logs a warning and fills data['stockWarnings'].
Every successful update also refreshes an existing order document.
beforeDelete() snapshots the voucher FKs, is_cancelled and stock_state by raw SQL before
the cascade delete of cms_mod_eshop_order_voucher_rel (the rows are gone by completedDelete()).
completedDelete() calls OrderStockManager::releaseDeletedOrder() per order and decrements
the vouchers from the snapshot, except for canceled orders (the cancellation already did). Both
hooks take the order id from the DeleteTrait snapshot, never from the removed entity.
Tags
Table of Contents
Constants
- DEFAULT_UNPAID_CANCEL_AFTER_DAYS : int = 30
- Fallback of {@see UNPAID_CANCEL_SETTING_KEY} when the row is missing or not a positive number (user decision 18. 10.: 30 days).
- UNPAID_CANCEL_SETTING_KEY : string = 'order.unpaid.cancelAfterDays'
- Setting (module {@see UNPAID_CANCEL_SETTING_MODULE}, e-shop) with the days after creation an order may stay awaiting payment before the cron cancels it. Edited in Eshop › Settings › Orders.
- DELETED_IDS_META : string = '__deletedIds'
- Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
- BUILD_STOCK_SOURCE_MODULE : int = 4
- `cms_system_modules` id of the eshop, the reservation source in the Store journal.
- BUILD_STOCK_SOURCE_TYPE : string = 'order'
- EXPIRE_DEFAULT_BATCH : int = 50
- EXPIRE_QUEUE : string = 'eshop.order.expire'
- EXPIRED_STATUS_KEY : string = 'EXPIRED'
- INVOICE_DEFAULT_BATCH : int = 20
- INVOICE_QUEUE : string = 'eshop.order.invoice'
- ORDERS_TABLE : string = 'cms_mod_eshop_orders'
- STATUSES_TABLE : string = 'cms_mod_eshop_order_statuses'
- UNPAID_CANCEL_DEFAULT_BATCH : int = 50
- UNPAID_CANCEL_QUEUE : string = 'eshop.order.cancel-unpaid'
- UNPAID_CANCEL_SETTING_MODULE : int = 4
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}.
- $blacklistManager : BlacklistManager|null
- $cacheTagInvalidator : CacheTagInvalidator|null
- Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
- $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 written by `OrderStockManager` through {@see updateColumns()}.
- $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
- $calculator : OrderCalculator
- $cronFailureManager : CronFailureManager
- $currencyManager : CurrencyManager
- $customerManager : OrderCustomerManager
- $em : EntityManagerInterface
- $invoiceGenerator : OrderInvoiceGenerator
- $languageManager : LanguageManager
- $localizationManager : LocalizationManager
- $numberGenerator : OrderNumberGenerator
- $orderPayloadHelper : OrderPayloadHelper
- $orderStatusManager : OrderStatusManager
- $orderTaxManager : OrderTaxManager
- $orderTransitionManager : OrderTransitionManager
- $paymentRegionManager : PaymentRegionManager
- $paymentRegionService : OrderPaymentRegionService
- $pickupPointApplier : OrderPickupPointApplier
- $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`.
- $snapshotBuilder : OrderSnapshotBuilder
- $stockItemManager : StockItemManager
- $stockManager : OrderStockManager
- $stockReservationManager : StockReservationManager
- $unitOfWorkReset : UnitOfWorkReset
- $userGroupManager : UserGroupManager
- $userTypeManager : UserTypeManager
- $voucherManager : VoucherManager
Methods
- __construct() : mixed
-
cancelUnpaid()
: array{processed: int, cancelled: int, skipped: int, errors: list
} - Cancels orders that stayed awaiting payment (card/gateway or bank transfer) for {@see unpaidCancelAfterDays()} days since creation (cron `/cron/eshop/order/cancel-unpaid`).
- 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
-
expireUnpaid()
: array{processed: int, expired: int, skipped: int, errors: list
} - Expires unpaid orders periodically (cron `/cron/eshop/order/expire`).
- find() : T|null
-
findAll()
: T> : list
>) - findBy() : T|null
- 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
-
getBuildData()
: array{success: bool, message?: string, errorCode?: string, id?: int|null, order?: array
, warnings?: list } - Builds, recalculates and optionally saves an order from the trusted rows of the admin edit form.
- 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`.
- getItemPrefillData() : array{success: bool, message?: string, errorCode?: string, code?: ?string, name?: string, unit?: ?string, vat?: string, price?: string}
- Returns prefill data for the order item picker (`api/eshop/order/item-prefill`).
- 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
- isOwnedBy() : bool
- Returns whether order `$orderId` belongs to user `$userId`; the single definition of order ownership.
- ownershipMatches() : bool
- Pure, stateless ownership match, testable without a DB.
- registerEvent() : Result
-
runInvoiceCatchup()
: array{processed: int, generated: int, permanent: int, skipped: int, errors: list
} - Issues invoices for orders catch-up style (cron `/cron/eshop/order/invoice`).
- save() : Result
- setAppContext() : $this
- setAssetFormatRegistry() : static
- setCacheTagInvalidator() : static
- setInternalCallContext() : static
- setLanguageContext() : $this
- setParamsConfig() : static
- setPresenterContext() : $this
- setSettingContext() : $this
- unpaidCancelAfterDays() : int
- Days an order may stay awaiting payment before {@see cancelUnpaid()} cancels it: the setting {@see UNPAID_CANCEL_SETTING_KEY}, else {@see DEFAULT_UNPAID_CANCEL_AFTER_DAYS}.
- updateAllRelations() : Result
- updateColumn() : Result
- updateColumns() : Result
- updateRelations() : Result
- updateStatusForPayment() : Result
- Fills the payment region of orders paired with a payment; called by the gateway callback {@see EshopPaymentHook::handlePayment()} (`callback_key = ESHOP_ORDER`).
- beforeDelete() : Result|null
- Snapshots what `completedDelete()` needs per order while the rows exist: the voucher FKs (before the `ON DELETE CASCADE` of `cms_mod_eshop_order_voucher_rel`), `is_cancelled` and `stock_state`. It runs post-commit, where a lazy load would find nothing and the removed entity has no id.
- beforeSave() : Result|null
- 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.
- completedDelete() : void
- Releases stock and decrements vouchers per deleted order, only from the values captured before the delete: the id from the `DeleteTrait` snapshot (Doctrine nulls the id of the removed instance) and the `beforeDelete()` state. The Store `SourceRef` survives the order delete (Store has no FK to the e-shop), so the release by id still works.
- completedSave() : void
- Post-commit stock handling (best-effort, see the class docblock). `$meta['skipReserve']` skips reserve and rereserve completely; `$meta['allowPartial']` (bool) overrides the default resolution in {@see OrderStockManager}.
- 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.
- 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_*`).
- applyStockResult() : void
- A stock operation failure must never fail an already committed save or delete; it only logs a warning and appends it to `Result::data['stockWarnings']` (the order is valid unreserved, `stock_state` says so).
- blacklistRefusal() : Result|null
- buildFailureArray() : array{success: false, errorCode: string, message: string}
- buildNumOrZero() : numeric-string
- Converts a DBAL/bcmath scalar (mixed) to a numeric-string, guarded (like `OrderCalculator::norm()`).
- buildRef() : T
- Returns a managed reference without a DB query (like {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder::ref()}).
- 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.
- cancelDueUnpaidOrderIds() : array<int, int>
- Returns orders awaiting payment created before the cutoff, oldest first (read only; the fresh load is done by `apply()`). Parked orders arrive as an id list.
- 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.
- checkStrictStockGate() : array{success: false, errorCode: string, message: string}|null
- Strict stock gate: called on `persist=true` before `OrderManager::save()`, only when `!$allowPartial`.
- columnsInvalidateCache() : bool
- currentUnitsSnapshot() : array<int, int>
- 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.
- dueInvoiceOrders() : array<int, TOrderGraph>
- Returns orders of the given status that have no handled invoice for it yet (`invoiced_status` is null or differs from the status), like {@see \App\UI\Api\Invoicer\Models\Managers\InvoiceManager} `duePayments()`.
- expireDueOrderIds() : array<int, int>
- Returns unpaid orders past `reservation_expires_at` whose current status allows cancelling (`allow_cancelling=1`), via raw SQL (read only; the fresh load is done by `apply()`).
- fetchUnitsSnapshot() : array<int, int>
- fetchVoucherIdsBefore() : array<int, int>
- Returns ids of live vouchers applied to the order according to the DB, not the in-memory entity. The update branch of `beforeSave()` diffs against it, and `beforeDelete()` needs it before the cascade delete of `cms_mod_eshop_order_voucher_rel`.
- findExpiredStatus() : TOrderStatusGraph|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.
- 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.
- loadPreviousCustomerLink() : array{0: ?int, 1: ?string, 2: ?int}
- Reads the previous customer link state before the Doctrine entity is loaded.
- logInvoiceWarning() : void
- Fail-soft log plus `Result::data['invoiceWarnings']` (same as `stockWarnings`).
- orderIdsForPayment() : array<int, int>
- Returns ids of orders paired with the payment. The authoritative source is the join table `cms_mod_eshop_order_payments`, not the denormalized `Order.payment` (only the last paired payment); one payment may theoretically cover several orders.
- orderVoucherIds() : array<int, int>
- Returns unique ids of live vouchers (the FK really exists) in the in-memory collection `$entity->getVouchers()`. A snapshot without a live voucher is skipped because there is no counter to adjust.
- ownActiveReservations() : array<int, string>
- Returns the sum of this order's active (`status=1`) reservations per stock item.
- regenerateOrderDocument() : void
- Refreshes the order document, see {@see OrderInvoiceGenerator::regenerateOrderDocument()}.
- resolveAllowPartial() : bool
- Resolves `allowPartial`: the payload override (bool, only when explicitly `true`/`false`), otherwise `4/order.partial.adminAllowed`.
- resolveBuyerType() : int
- Computes `buyerType` (0 = natural person, 1 = company) server-side; it is not a form field.
- resolveCustomerLink() : array{0: ?int, 1: ?int}
- Resolves the card-file customer and contact for the order being built.
- resolveDefaultPaymentRegionId() : int
- Resolves the VAT payment region for a new snapshot: setting `4/invoicer.paymentRegionId`, then `7/invoice.default.paymentRegionId` (no prefix), then the localization default ({@see PaymentRegionManager::getDefaultPaymentRegionId()}).
- rollbackBatch() : void
- saveWithHooks() : Result
- Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
- statusesRequiringInvoice() : array<int, OrderStatus>
- Returns active statuses with `createInvoice=1` (the batch is a budget across them, like {@see \App\UI\Api\Invoicer\Models\Managers\InvoiceManager} `rulesByStatus()`).
- touchOrderCollectionsForInvoice() : void
- Forces hydration of all order collections that {@see OrderInvoiceGenerator::appendLines()} reads, before any `find()`/`findAll()` inside the generator.
- usesAssetPathTrait() : bool
Constants
DEFAULT_UNPAID_CANCEL_AFTER_DAYS
Fallback of {@see UNPAID_CANCEL_SETTING_KEY} when the row is missing or not a positive number (user decision 18. 10.: 30 days).
public
int
DEFAULT_UNPAID_CANCEL_AFTER_DAYS
= 30
UNPAID_CANCEL_SETTING_KEY
Setting (module {@see UNPAID_CANCEL_SETTING_MODULE}, e-shop) with the days after creation an order may stay awaiting payment before the cron cancels it. Edited in Eshop › Settings › Orders.
public
string
UNPAID_CANCEL_SETTING_KEY
= 'order.unpaid.cancelAfterDays'
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().
BUILD_STOCK_SOURCE_MODULE
`cms_system_modules` id of the eshop, the reservation source in the Store journal.
private
int
BUILD_STOCK_SOURCE_MODULE
= 4
BUILD_STOCK_SOURCE_TYPE
private
string
BUILD_STOCK_SOURCE_TYPE
= 'order'
EXPIRE_DEFAULT_BATCH
private
int
EXPIRE_DEFAULT_BATCH
= 50
EXPIRE_QUEUE
private
string
EXPIRE_QUEUE
= 'eshop.order.expire'
EXPIRED_STATUS_KEY
private
string
EXPIRED_STATUS_KEY
= 'EXPIRED'
INVOICE_DEFAULT_BATCH
private
int
INVOICE_DEFAULT_BATCH
= 20
INVOICE_QUEUE
private
string
INVOICE_QUEUE
= 'eshop.order.invoice'
ORDERS_TABLE
private
string
ORDERS_TABLE
= 'cms_mod_eshop_orders'
STATUSES_TABLE
private
string
STATUSES_TABLE
= 'cms_mod_eshop_order_statuses'
UNPAID_CANCEL_DEFAULT_BATCH
private
int
UNPAID_CANCEL_DEFAULT_BATCH
= 50
UNPAID_CANCEL_QUEUE
private
string
UNPAID_CANCEL_QUEUE
= 'eshop.order.cancel-unpaid'
UNPAID_CANCEL_SETTING_MODULE
private
int
UNPAID_CANCEL_SETTING_MODULE
= 4
Properties
$service
Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
public
Service<string|int, TEntity>
$service
$appContext
protected
AppContext
$appContext
$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.
$blacklistManager
protected
BlacklistManager|null
$blacklistManager
= null
$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.
$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.
$languageContext
protected
LanguageContext|null
$languageContext
= NULL
$nonInvalidatingColumns
Columns written by `OrderStockManager` through {@see updateColumns()}.
protected
array<string|int, string>
$nonInvalidatingColumns
= ['views', 'viewsList', 'viewsDetail', 'stockState', 'reservationExpiresAt', 'stockDocumentId']
They are stock state, not order content: bumping the cache tags of the whole Order
class on every reservation or release would break the order list query cache several
times per checkout, and updateColumns() runs inside runInSourceTransaction(), where
invalidating from an uncommitted transaction is what the cache revision avoids.
IMPORTANT: list the columns as entity properties; the mapper translates them to
stock_state, reservation_expires_at and stock_document_id.
$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.
$presenterContext
protected
PresenterContext
$presenterContext
$settingContext
protected
SettingContext
$settingContext
$totalCount
protected
int|null
$totalCount
= null
$useTransactionForDelete
protected
bool
$useTransactionForDelete
= true
$useTransactionForSave
protected
bool
$useTransactionForSave
= true
$calculator read-only
private
OrderCalculator
$calculator
$cronFailureManager read-only
private
CronFailureManager
$cronFailureManager
$currencyManager read-only
private
CurrencyManager
$currencyManager
$customerManager read-only
private
OrderCustomerManager
$customerManager
$em read-only
private
EntityManagerInterface
$em
$invoiceGenerator read-only
private
OrderInvoiceGenerator
$invoiceGenerator
$languageManager read-only
private
LanguageManager
$languageManager
$localizationManager read-only
private
LocalizationManager
$localizationManager
$numberGenerator read-only
private
OrderNumberGenerator
$numberGenerator
$orderPayloadHelper read-only
private
OrderPayloadHelper
$orderPayloadHelper
$orderStatusManager read-only
private
OrderStatusManager
$orderStatusManager
$orderTaxManager read-only
private
OrderTaxManager
$orderTaxManager
$orderTransitionManager read-only
private
OrderTransitionManager
$orderTransitionManager
$paymentRegionManager read-only
private
PaymentRegionManager
$paymentRegionManager
$paymentRegionService read-only
private
OrderPaymentRegionService
$paymentRegionService
$pickupPointApplier read-only
private
OrderPickupPointApplier
$pickupPointApplier
$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
= []
$snapshotBuilder read-only
private
OrderSnapshotBuilder
$snapshotBuilder
$stockItemManager read-only
private
StockItemManager
$stockItemManager
$stockManager read-only
private
OrderStockManager
$stockManager
$stockReservationManager read-only
private
StockReservationManager
$stockReservationManager
$unitOfWorkReset read-only
private
UnitOfWorkReset
$unitOfWorkReset
$userGroupManager read-only
private
UserGroupManager
$userGroupManager
$userTypeManager read-only
private
UserTypeManager
$userTypeManager
$voucherManager read-only
private
VoucherManager
$voucherManager
Methods
__construct()
public
__construct(OrderService $service, OrderNumberGenerator $numberGenerator, OrderCalculator $calculator, EntityManagerInterface $em, UnitOfWorkReset $unitOfWorkReset, OrderStockManager $stockManager, VoucherManager $voucherManager, OrderPaymentRegionService $paymentRegionService, OrderInvoiceGenerator $invoiceGenerator, OrderStatusManager $orderStatusManager, OrderTransitionManager $orderTransitionManager, OrderSnapshotBuilder $snapshotBuilder, OrderPayloadHelper $orderPayloadHelper, OrderCustomerManager $customerManager, CurrencyManager $currencyManager, LanguageManager $languageManager, LocalizationManager $localizationManager, UserTypeManager $userTypeManager, StockItemManager $stockItemManager, StockReservationManager $stockReservationManager, UserGroupManager $userGroupManager, PaymentRegionManager $paymentRegionManager, OrderTaxManager $orderTaxManager, OrderPickupPointApplier $pickupPointApplier, CronFailureManager $cronFailureManager[, BlacklistManager|null $blacklistManager = null ]) : mixed
Parameters
- $service : OrderService
- $numberGenerator : OrderNumberGenerator
- $calculator : OrderCalculator
- $em : EntityManagerInterface
- $unitOfWorkReset : UnitOfWorkReset
- $stockManager : OrderStockManager
- $voucherManager : VoucherManager
- $paymentRegionService : OrderPaymentRegionService
- $invoiceGenerator : OrderInvoiceGenerator
- $orderStatusManager : OrderStatusManager
- $orderTransitionManager : OrderTransitionManager
- $snapshotBuilder : OrderSnapshotBuilder
- $orderPayloadHelper : OrderPayloadHelper
- $customerManager : OrderCustomerManager
- $currencyManager : CurrencyManager
- $languageManager : LanguageManager
- $localizationManager : LocalizationManager
- $userTypeManager : UserTypeManager
- $stockItemManager : StockItemManager
- $stockReservationManager : StockReservationManager
- $userGroupManager : UserGroupManager
- $paymentRegionManager : PaymentRegionManager
- $orderTaxManager : OrderTaxManager
- $pickupPointApplier : OrderPickupPointApplier
- $cronFailureManager : CronFailureManager
- $blacklistManager : BlacklistManager|null = null
cancelUnpaid()
Cancels orders that stayed awaiting payment (card/gateway or bank transfer) for {@see unpaidCancelAfterDays()} days since creation (cron `/cron/eshop/order/cancel-unpaid`).
public
cancelUnpaid([int|null $limit = null ]) : array{processed: int, cancelled: int, skipped: int, errors: list}
Like expireUnpaid() it has no cancellation logic of its own: it selects due ids with raw
SQL over the module's tables (OrderTransitionManager::UNPAID_CANCEL_DUE_PREDICATE) and
calls only OrderTransitionManager::apply() to the EXPIRED status with the opt
cancelUnpaidCreatedBefore, which re-checks under the lock, refuses an order whose payment
already brought money (paymentReceived → back-off and an admin notification once attempts are
exhausted), releases the reservation, gives back vouchers and queues the status e-mail of
EXPIRED ("not paid within the period, cancelled"). A late gateway payment then leaves the
order cancelled and alerts the admin (EshopPaymentHook),
and the gateway refuses to start a new payment for it (isPaymentPayable()).
Idempotent: is_cancelled=1 removes the order from the next selection and apply() is a no-op
on the same status, so a second run sends no second e-mail.
Parameters
- $limit : int|null = null
Return values
array{processed: int, cancelled: int, skipped: int, errors: listdelete()
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): ?Resultruns before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows intoafterDeleteunder$meta['__beforeDelete'].afterDelete(array $entities, Result $result, array $meta): ?Resultruns 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 inbeforeDelete(collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only intocompletedDelete.completedDelete(array $entities, Result $result, array $meta): voidruns best effort after the commit, outside the transaction; an exception is only logged (the delete stays).completedAssetCleanup(array $entities, Result $result, array $meta): voidruns best effort after the commit and releases theAssetrows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in isownedAssetIds(object $entity): arrayon 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
ResultdeleteAll()
public
deleteAll([array<string|int, mixed> $filters = [] ]) : Result
Parameters
- $filters : array<string|int, mixed> = []
Return values
ResultexpireUnpaid()
Expires unpaid orders periodically (cron `/cron/eshop/order/expire`).
public
expireUnpaid([int|null $limit = null ]) : array{processed: int, expired: int, skipped: int, errors: list}
Has no cancellation logic of its own: it selects due order ids with raw SQL (read only) and
calls only OrderTransitionManager::apply() to the EXPIRED status (OrderStatus::$key),
which handles the fresh load under lock, gating, reservation release, voucher decrement
(cancelOrder=1) and the e-mail. Idempotent: apply() is a no-op on an already-target status
and is_cancelled=1 removes the order from the next selection (see
expireDueOrderIds()), so two consecutive runs produce no second effect.
The scope is deliberately limited to statuses with allow_cancelling=1 (currently
CREATED/APPROVED, resolved by JOIN, not hardcoded ids): checkGating() would reject anything
else (e.g. WAITING-ON-PAYMENT with a proforma already issued) with transitionNotAllowed.
The underlying stock reservation of WAITING-ON-PAYMENT orders expires independently via the
Store cron /cron/store/reservation/expire, so this limit does not endanger stock.
Parameters
- $limit : int|null = null
Return values
array{processed: int, expired: int, skipped: int, errors: listfind()
public
find(int $id[, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
- $id : int
- $includes : string|array<string|int, mixed> = []
Return values
T|nullfindAll()
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> : listentities, 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|nullfindRelatedIds()
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)
getAppContext()
public
getAppContext() : AppContext
Return values
AppContextgetBuildData()
Builds, recalculates and optionally saves an order from the trusted rows of the admin edit form.
public
getBuildData(array<string, mixed> $payload) : array{success: bool, message?: string, errorCode?: string, id?: int|null, order?: array, warnings?: list}
The catalog only prefills (see getItemPrefillData() and the prefill inside
buildShipperFromRow()/buildPaymentMethodFromRow() when price is omitted); a save never
silently reprices from the current catalog, what is in the form is stored. The only automation
is the VAT computation from base and rate, driven by the order-level withVat.
persist=false never calls flush() (read-only getReference()/raw SELECT), so a preview has
no side effects even over an existing (managed) order.
payload:
id(?int): edited order (null = new),statusId(?int): only on insert; on update the status never changes here (only OrderTransitionManager does that),customer(array):companyName/regNo/vatId/firstname/lastname/street/city/postcode/country/email/phone/informations, plus optionalcustomerId(manual pick of a card-file customer; wins over e-mail matching,persist=trueonly) and optionalcustomerContactId(manual pick of a concrete customer address; the server checks ownership, a foreign or missing id silently falls back to the first contact, see OrderCustomerManager::manualSelection()),delivery(array):name/street/city/postcode/country/informations,seller(array): seller snapshot withsellerId(required whenpersist=true, otherwisesellerRequired), referentialsellerContactId/sellerBankAccountIdand manually editablename/street/city/postcode/country/regNo/vatId/email/phone/informations/vatInformations/bankName/bankAccount/iban/swift. A key missing from the payload leaves the snapshot on the order unchanged (no reset),userId/currencyId/languageId/userTypeId/paymentRegionId(?int):userIdalso feeds resolveCustomerLink(); a newly created card-file customer (no contact found by e-mail) is linked to it right away (fill-on-create, never overwrites an existing one),withVat(bool),note(?string) which goes toorder.informations.buyerTypeis not a form input: the Api computes it server-side, see resolveBuyerType(), and a payloadbuyerTypeis ignored,lines(list<array{setId:int, quantity:int, price:string(base), vat:string, orderItemId?:int, name?:string, code?:string, unit?:string, discountPercents?:string}>):priceis required (missing or invalid giveslinePriceRequired, no silent fallback to the catalog);orderItemIdis an edited existing item and is carried over (see OrderSnapshotBuilder::buildItemsFromRows()),shipping(array{tariffId:?int, price?:string, vat?:string}): an omitted ornullpriceis computed viaShippingPriceResolver(prefill, incl. free_from); a given one is trusted verbatim,payment(array{paymentMethodId:?int, price?:string, vat?:string}): same logic,allowUnavailablePaymentMethod(bool): the admin always sendstrue(trusted admin path), the front checkout sendsfalse, the gate in OrderSnapshotBuilder is unchanged. When a method withavailable=0is saved only thanks to this override, the response carries it inwarnings,voucherCodes(list), gifts(list<array{giftId:int, amount?:int}>),pickupPoint(?array<string, mixed>): the raw carrier widget object as issued (the admin keeps it in the hiddenpickupPoint); only the driver normalizes it, byShipper::$scriptName, never the admin or JS,pickupPointManual(?array{code,name,street,city,zip,country}): manual entry by the admin. Applies only whenpickupPointis missing or the driver does not normalize it (the raw payload wins). A tariff withoutexternal_pickupsilently ignores both fields; a tariff withexternal_pickupand no usable point givespickupPointRequired(persist=trueonly). A carrier without a driver does not block manual entry: the snapshot getspickup_carrier = 'manual',allowPartial(?bool): override of4/order.partial.adminAllowed(persist=trueonly),persist(bool, default false).
warnings in the response are non-blocking notices (success stays true), a list of codes:
paymentMethodUnavailable (the chosen payment method has available=0, saved only thanks to
allowUnavailablePaymentMethod). The admin OrderFormTrait translates and shows them.
Parameters
- $payload : array<string, mixed>
Return values
array{success: bool, message?: string, errorCode?: string, id?: int|null, order?: arraygetDefaultSkipFields()
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 scalaruserIdhas an#[ORM\AttributeOverride]withinsertable: false, updatable: falsein the Api leaf and the column belongs to the association$user, so['user' => ['id' => $userId]]; ComcatResponsehas no$userassociation, 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>getItemPrefillData()
Returns prefill data for the order item picker (`api/eshop/order/item-prefill`).
public
getItemPrefillData(int $setId, int $quantity, int $currencyId, int $languageId[, int|null $paymentRegionId = null ]) : array{success: bool, message?: string, errorCode?: string, code?: ?string, name?: string, unit?: ?string, vat?: string, price?: string}
The single data source for the OrderItemPickerTrait force-submit: the admin cannot compute
catalog prices/VAT itself. Saves nothing, only resolves values as of now.
Parameters
- $setId : int
- $quantity : int
- $currencyId : int
- $languageId : int
- $paymentRegionId : int|null = null
Return values
array{success: bool, message?: string, errorCode?: string, code?: ?string, name?: string, unit?: ?string, vat?: string, price?: string}getLanguageContext()
public
getLanguageContext() : LanguageContext|null
Return values
LanguageContext|nullgetPresenterContext()
public
getPresenterContext() : PresenterContext
Return values
PresenterContextgetServiceClass()
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-stringgetSettingContext()
public
getSettingContext() : SettingContext
Return values
SettingContextgetSettings()
public
getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>getTotalCount()
public
getTotalCount() : int|null
Return values
int|nullinitializeAllContexts()
public
initializeAllContexts() : void
isOwnedBy()
Returns whether order `$orderId` belongs to user `$userId`; the single definition of order ownership.
public
isOwnedBy(int $orderId, int $userId) : bool
Used by the RBAC gate of the external Api path (OrderPresenter::verifyOwnership()), by parent ownership of a complaint (ComplaintManager::canUserWrite()) and by the front (OrderCheckoutManager::summary(), OrderCheckoutManager::repeatItems()), which call the pure self::ownershipMatches() on an already loaded row.
IMPORTANT: the owner is read from the DB by $orderId; data.user.id or data.userId
from the payload never reaches this method, so a caller cannot claim another identity.
IMPORTANT: a guest order (cms_system_users_user_id IS NULL) belongs to nobody and
returns false even for a logged-in user. Its ownership is held by the front session
(CartManager::ownsOrder()) and the
external Api path must not touch it.
Uses raw DBAL instead of find(): only one column is needed and find() would hydrate
the whole order aggregate, inside startup() before any action.
Parameters
- $orderId : int
- $userId : int
Return values
boolownershipMatches()
Pure, stateless ownership match, testable without a DB.
public
static ownershipMatches(int|null $recordUserId, int $userId) : bool
A null owner (guest order) or 0 never matches, not even against $userId === 0
(an anonymous caller: getUser()->getId() returns null, not 0).
Parameters
- $recordUserId : int|null
- $userId : int
Return values
boolregisterEvent()
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
ResultrunInvoiceCatchup()
Issues invoices for orders catch-up style (cron `/cron/eshop/order/invoice`).
public
runInvoiceCatchup([int|null $limit = null ]) : array{processed: int, generated: int, permanent: int, skipped: int, errors: list}
A watermark reconcile of the same shape as
InvoiceManager::processPaidPayments(), but over
orders: a batch of orders whose current status has createInvoice=1 and whose
invoiced_status differs from that status id (null means never invoiced for this status).
Has no invoicing logic of its own: it calls only
OrderInvoiceGenerator::generateForStatus(), the same
generator OrderTransitionManager uses; the generator writes invoiced_status itself on
success and when it finds an existing invoice.
Permanent vs transient failures (errorCode from the generator): the codes in
OrderInvoiceGenerator::PERMANENT_ERROR_CODES (for example sourceInvoiceMissing,
negativeTotal) are permanent (retrying does not help), so the watermark still advances,
otherwise the order would eat the batch forever; the admin resolves it manually (see errors
in the stats). Anything else (missing seller configuration, DB error, \Throwable) is
transient: the watermark is left alone and the order is retried next run.
IMPORTANT: the order includes must be OrderTransitionManager::ORDER_INCLUDES (the same
list, notably gifts), otherwise the object graph is cleared incorrectly.
Parameters
- $limit : int|null = null
Return values
array{processed: int, generated: int, permanent: int, skipped: int, errors: listsave()
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
ResultsetAppContext()
public
setAppContext(AppContext $appContext) : $this
Parameters
- $appContext : AppContext
Return values
$thissetAssetFormatRegistry()
public
setAssetFormatRegistry(AssetFormatRegistry $assetFormatRegistry) : static
Parameters
- $assetFormatRegistry : AssetFormatRegistry
Return values
staticsetCacheTagInvalidator()
public
setCacheTagInvalidator(CacheTagInvalidator $cacheTagInvalidator) : static
Parameters
- $cacheTagInvalidator : CacheTagInvalidator
Return values
staticsetInternalCallContext()
public
setInternalCallContext(InternalCallContext $internalCallContext) : static
Parameters
- $internalCallContext : InternalCallContext
Return values
staticsetLanguageContext()
public
setLanguageContext(LanguageContext|null $languageContext) : $this
Parameters
- $languageContext : LanguageContext|null
Return values
$thissetParamsConfig()
public
setParamsConfig(Config $paramsConfig) : static
Parameters
- $paramsConfig : Config
Return values
staticsetPresenterContext()
public
setPresenterContext(PresenterContext $presenterContext) : $this
Parameters
- $presenterContext : PresenterContext
Return values
$thissetSettingContext()
public
setSettingContext(SettingContext $settingContext) : $this
Parameters
- $settingContext : SettingContext
Return values
$thisunpaidCancelAfterDays()
Days an order may stay awaiting payment before {@see cancelUnpaid()} cancels it: the setting {@see UNPAID_CANCEL_SETTING_KEY}, else {@see DEFAULT_UNPAID_CANCEL_AFTER_DAYS}.
public
unpaidCancelAfterDays() : int
IMPORTANT: zero or a negative value never passes through (the admin form allows 1–3650). The status
awaiting payment cannot be cancelled by hand (allow_cancelling=0), so "never cancel" would leave
such orders hanging forever; 0 would cancel every order awaiting payment on the next run.
Return values
intupdateAllRelations()
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
ResultupdateColumn()
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
ResultupdateColumns()
public
updateColumns(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result
Parameters
- $entityId : int|array<string|int, mixed>
-
id, or a list of ids / composite-id arrays
- $values : array<string, mixed>
-
property name => new value
Return values
ResultupdateRelations()
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
ResultupdateStatusForPayment()
Fills the payment region of orders paired with a payment; called by the gateway callback {@see EshopPaymentHook::handlePayment()} (`callback_key = ESHOP_ORDER`).
public
updateStatusForPayment(Payment $payment) : Result
Acts only on a completed payment and is otherwise a no-op success, so a repeated call is
harmless. It is a no-op until
an order is paired with the payment (a row in cms_mod_eshop_order_payments, created by
the front checkout or manually through OrderTransitionManager).
Fill-if-empty: fills the payment region of every paired order that has none, through
updateColumn() on the FK (no object-graph save(), so no orphan removal risk).
An admin's manual value is never overwritten; OrderPaymentRegionService decides.
Parameters
- $payment : Payment
Return values
ResultbeforeDelete()
Snapshots what `completedDelete()` needs per order while the rows exist: the voucher FKs (before the `ON DELETE CASCADE` of `cms_mod_eshop_order_voucher_rel`), `is_cancelled` and `stock_state`. It runs post-commit, where a lazy load would find nothing and the removed entity has no id.
protected
beforeDelete(array<int, TOrderGraph> $entities[, array<string, mixed> &$meta = [] ]) : Result|null
The state is read from the DB by raw SQL, like in beforeSave() and OrderStockManager:
the in-memory entity may be stale. Keyed by the id from the DeleteTrait snapshot and stored
in $meta['_ordersBeforeDelete'].
Parameters
- $entities : array<int, TOrderGraph>
- $meta : array<string, mixed> = []
Return values
Result|nullbeforeSave()
protected
beforeSave(bool $isInsert, TOrderGraph &$entity[, array<string, mixed> &$meta = [] ]) : Result|null
Parameters
- $isInsert : bool
- $entity : TOrderGraph
- $meta : array<string, mixed> = []
Return values
Result|nullbeginRejectedUploads()
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>completedDelete()
Releases stock and decrements vouchers per deleted order, only from the values captured before the delete: the id from the `DeleteTrait` snapshot (Doctrine nulls the id of the removed instance) and the `beforeDelete()` state. The Store `SourceRef` survives the order delete (Store has no FK to the e-shop), so the release by id still works.
protected
completedDelete(array<int, Order> $entities, Result $result[, array<string, mixed> $meta = [] ]) : void
Parameters
completedSave()
Post-commit stock handling (best-effort, see the class docblock). `$meta['skipReserve']` skips reserve and rereserve completely; `$meta['allowPartial']` (bool) overrides the default resolution in {@see OrderStockManager}.
protected
completedSave(bool $isInsert, Order $entity, Result $result[, array<string, mixed> $meta = [] ]) : void
Parameters
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|nullgetService()
Returns the service backing the manager traits.
protected
getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>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
pathit is checked before reading
Return values
string|nullrecordRejectedUpload()
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
applyStockResult()
A stock operation failure must never fail an already committed save or delete; it only logs a warning and appends it to `Result::data['stockWarnings']` (the order is valid unreserved, `stock_state` says so).
private
applyStockResult(int $orderId, Result $stockResult, string $operation, Result $saveResult) : void
Parameters
blacklistRefusal()
private
blacklistRefusal(string|null $email[, string|null $phone = null ][, string|null $ip = null ]) : Result|null
Parameters
- $email : string|null
- $phone : string|null = null
- $ip : string|null = null
Return values
Result|nullbuildFailureArray()
private
buildFailureArray(Result $result) : array{success: false, errorCode: string, message: string}
Parameters
- $result : Result
Return values
array{success: false, errorCode: string, message: string}buildNumOrZero()
Converts a DBAL/bcmath scalar (mixed) to a numeric-string, guarded (like `OrderCalculator::norm()`).
private
buildNumOrZero(mixed $value) : numeric-string
Parameters
- $value : mixed
Return values
numeric-stringbuildRef()
Returns a managed reference without a DB query (like {@see \App\UI\Api\Eshop\Models\Services\OrderSnapshotBuilder::ref()}).
private
buildRef(T> $class, int $id) : T
Parameters
- $class : T>
- $id : int
Tags
Return values
TbumpDeleteCacheTags()
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>
cancelDueUnpaidOrderIds()
Returns orders awaiting payment created before the cutoff, oldest first (read only; the fresh load is done by `apply()`). Parked orders arrive as an id list.
private
cancelDueUnpaidOrderIds(DateTimeImmutable $createdBefore, int $limit[, array<int, int> $parkedIds = [] ]) : array<int, int>
Parameters
- $createdBefore : DateTimeImmutable
- $limit : int
- $parkedIds : array<int, int> = []
Return values
array<int, int>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>checkStrictStockGate()
Strict stock gate: called on `persist=true` before `OrderManager::save()`, only when `!$allowPartial`.
private
checkStrictStockGate(Order $order, bool $allowPartial) : array{success: false, errorCode: string, message: string}|null
Partial mode is unchanged (OrderTransitionManager/restore untouched). An unmapped variant
(notInStock > 0 on the builder) behaves exactly like zero stock. It pre-flights the sum of
units per stockItemId against StockReader::getAvailabilityBatch(); on update it adds
back the order's own active reservations (otherwise a quantity edit would collide with its own
old reservation, which rereserve() releases only after this gate). A race window between
the pre-flight and completedSave->reserve remains: reserve() is the final arbiter and
a stock failure does not fail the save; the gate only moves the decision before the write.
Parameters
- $order : Order
- $allowPartial : bool
Return values
array{success: false, errorCode: string, message: string}|null —null = gate projde
columnsInvalidateCache()
private
columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
- $columns : array<int, string>
Return values
boolcurrentUnitsSnapshot()
private
currentUnitsSnapshot(Order $entity) : array<int, int>
Parameters
- $entity : Order
Return values
array<int, int> —stockItemId => sum of quantity (in-memory entity after the save)
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
ResultdueInvoiceOrders()
Returns orders of the given status that have no handled invoice for it yet (`invoiced_status` is null or differs from the status), like {@see \App\UI\Api\Invoicer\Models\Managers\InvoiceManager} `duePayments()`.
private
dueInvoiceOrders(OrderStatus $status, int $limit[, array<int, int> $parkedIds = [] ]) : array<int, TOrderGraph>
Parked orders are skipped; ordered by id (deterministic).
Parameters
- $status : OrderStatus
- $limit : int
- $parkedIds : array<int, int> = []
Return values
array<int, TOrderGraph>expireDueOrderIds()
Returns unpaid orders past `reservation_expires_at` whose current status allows cancelling (`allow_cancelling=1`), via raw SQL (read only; the fresh load is done by `apply()`).
private
expireDueOrderIds(int $limit[, array<int, int> $parkedIds = [] ]) : array<int, int>
Uses the module's own tables (cms_mod_eshop_*), so the layer boundary guard does not
report it. Parked orders arrive as an id list, not a JOIN on a foreign tracking table.
Parameters
- $limit : int
- $parkedIds : array<int, int> = []
Return values
array<int, int>fetchUnitsSnapshot()
private
fetchUnitsSnapshot(int $orderId) : array<int, int>
Parameters
- $orderId : int
Return values
array<int, int> —stockItemId => sum of quantity (order_item_units in the DB before the save)
fetchVoucherIdsBefore()
Returns ids of live vouchers applied to the order according to the DB, not the in-memory entity. The update branch of `beforeSave()` diffs against it, and `beforeDelete()` needs it before the cascade delete of `cms_mod_eshop_order_voucher_rel`.
private
fetchVoucherIdsBefore(int $orderId) : array<int, int>
Parameters
- $orderId : int
Return values
array<int, int>findExpiredStatus()
private
findExpiredStatus() : TOrderStatusGraph|null
Return values
TOrderStatusGraph|nullisAssetPathAllowed()
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
boolloadEntitiesForCacheTags()
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>loadPreviousCustomerLink()
Reads the previous customer link state before the Doctrine entity is loaded.
private
loadPreviousCustomerLink(int|null $orderId) : array{0: ?int, 1: ?string, 2: ?int}
It must be raw DBAL, not $this->find() (see resolveCustomerLink()). A new order
($orderId === null, or the row was deleted meanwhile) gives [null, null, null].
Parameters
- $orderId : int|null
Return values
array{0: ?int, 1: ?string, 2: ?int} —customerId, customerEmail, customerContactId
logInvoiceWarning()
Fail-soft log plus `Result::data['invoiceWarnings']` (same as `stockWarnings`).
private
logInvoiceWarning(TOrderGraph $entity, Result $result, string $suffix) : void
Parameters
- $entity : TOrderGraph
- $result : Result
- $suffix : string
orderIdsForPayment()
Returns ids of orders paired with the payment. The authoritative source is the join table `cms_mod_eshop_order_payments`, not the denormalized `Order.payment` (only the last paired payment); one payment may theoretically cover several orders.
private
orderIdsForPayment(int $paymentId) : array<int, int>
Parameters
- $paymentId : int
Return values
array<int, int>orderVoucherIds()
Returns unique ids of live vouchers (the FK really exists) in the in-memory collection `$entity->getVouchers()`. A snapshot without a live voucher is skipped because there is no counter to adjust.
private
orderVoucherIds(TOrderGraph $entity) : array<int, int>
Parameters
- $entity : TOrderGraph
Return values
array<int, int>ownActiveReservations()
Returns the sum of this order's active (`status=1`) reservations per stock item.
private
ownActiveReservations(int $orderId) : array<int, string>
On update it is added back to availability: the edit would otherwise collide with its own old
reservation, which rereserve() releases and recreates later in the same save operation.
Parameters
- $orderId : int
Return values
array<int, string> —stockItemId => numeric-string
regenerateOrderDocument()
Refreshes the order document, see {@see OrderInvoiceGenerator::regenerateOrderDocument()}.
private
regenerateOrderDocument(TOrderGraph $entity, Result $result) : void
A no-op when the order has no order document yet. Fail-soft: a failed refresh must never fail an already committed order edit.
Parameters
- $entity : TOrderGraph
- $result : Result
resolveAllowPartial()
Resolves `allowPartial`: the payload override (bool, only when explicitly `true`/`false`), otherwise `4/order.partial.adminAllowed`.
private
resolveAllowPartial(array<string, mixed> $payload) : bool
The same value goes both to the gate (checkStrictStockGate()) and to
OrderManager::save()'s $meta['allowPartial']; the gate and the actual
reserve()/rereserve() must not diverge.
Parameters
- $payload : array<string, mixed>
Return values
boolresolveBuyerType()
Computes `buyerType` (0 = natural person, 1 = company) server-side; it is not a form field.
private
resolveBuyerType(int|null $userTypeId, array<string|int, mixed> $customer) : int
- A selected
userTypeIddecides viaUserType.requiresCompanyData()(see UserType). - Without a selected or existing type it falls back to whether the admin filled
customer.companyNameorcustomer.regNo.
The payload key buyerType is never read; the server is the only authority.
IMPORTANT: like resolveCustomerLink(), call it before $this->find($id, ...).
UserTypeManager::find() goes through GetAllTrait::clearDoctrine() and over a loaded order
graph would detach it, so the flush then fails with ORMInvalidArgumentException: another object of class OrderShipper was already present for the same ID (details at the call in
getBuildData()). It therefore takes only primitives (?int and the $customer array),
no Doctrine entity.
Parameters
- $userTypeId : int|null
- $customer : array<string|int, mixed>
Return values
intresolveCustomerLink()
Resolves the card-file customer and contact for the order being built.
private
resolveCustomerLink(int|null $orderId, array<string|int, mixed> $customer[, int|null $userId = null ]) : array{0: ?int, 1: ?int}
A manual pick from the admin picker/select wins over the resolver when the payload carries
a real change against the stored state: a new customerId, or the same customer with
a different customerContactId (the admin only switched the address). Otherwise
OrderCustomerManager::resolve() runs only when the e-mail really changed (always for
a new order, where $previousCustomerId === null). With no e-mail change and no manual pick,
customerId/customerContactId stay as stored (edit) or null (add without e-mail), which
keeps a plain re-save idempotent.
IMPORTANT: call it before $this->find($id, ...) (see the caller getBuildData()), never over
a loaded entity. OrderCustomerManager::resolve()/manualSelection() call Api manager
findAll()/save() internally, and GetAllTrait::clearDoctrine() clears the whole Doctrine
identity map. A loaded order with an eagerly loaded shipper/paymentMethod would be detached
and the next flush() would fail with ORMInvalidArgumentException: another object of class OrderShipper was already present for the same ID (this also hit the original e-mail matching on
editing an order with a shipper). It therefore takes only primitives ($orderId, the $customer
array), no Doctrine entity, so it cannot detach anything.
Parameters
- $orderId : int|null
- $customer : array<string|int, mixed>
- $userId : int|null = null
-
payload
userId(the order's user account), passed to OrderCustomerManager::resolve() for fill-on-create linking of a newly created customer; themanualSelection()branch does not need it because no customer is created there
Return values
array{0: ?int, 1: ?int} —customerId, customerContactId
resolveDefaultPaymentRegionId()
Resolves the VAT payment region for a new snapshot: setting `4/invoicer.paymentRegionId`, then `7/invoice.default.paymentRegionId` (no prefix), then the localization default ({@see PaymentRegionManager::getDefaultPaymentRegionId()}).
private
resolveDefaultPaymentRegionId() : int
Return values
introllbackBatch()
private
rollbackBatch() : void
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
ResultstatusesRequiringInvoice()
Returns active statuses with `createInvoice=1` (the batch is a budget across them, like {@see \App\UI\Api\Invoicer\Models\Managers\InvoiceManager} `rulesByStatus()`).
private
statusesRequiringInvoice() : array<int, OrderStatus>
Return values
array<int, OrderStatus>touchOrderCollectionsForInvoice()
Forces hydration of all order collections that {@see OrderInvoiceGenerator::appendLines()} reads, before any `find()`/`findAll()` inside the generator.
private
touchOrderCollectionsForInvoice(TOrderGraph $entity) : void
IMPORTANT: this path (an edit outside a state transition) has no eager load like
ORDER_INCLUDES in OrderTransitionManager. Without it a lazy collection would be
hydrated after a foreign clearDoctrine() and its items would carry a detached owner,
so the flush would fail with "A new entity was found through the relationship".
Parameters
- $entity : TOrderGraph
usesAssetPathTrait()
private
static usesAssetPathTrait(string $class) : bool
Parameters
- $class : string