StockReader
in package
Read model for stock availability and delivery estimates. Pure reads, no locks.
The batch API runs a constant number of SQL queries regardless of the number of items (aggregation over levels, warehouses, items, suppliers and vacations), so an e-shop can query a whole category listing cheaply.
Delivery-days algorithm (deterministic; the vacation is opt-out per warehouse):
D_shiftstarts at today; while it falls within an active vacation it moves to the vacationend_date + 1(re-scanning handles consecutive windows).- For each active warehouse with
available > 0:D_wisD_shiftwhen the warehouse hasvacation_affected, otherwise today;ship_w = D_w + delivery_days. - With
available > 0, shipping ismin(ship_w). Otherwise, if the item has a supplier withdelivery_days, shipping isD_shift + supplier.delivery_days. OtherwisedeliveryDaysis null ("on request"). deliveryDaysis shipping minus today, in days.
A vacation shifts the shipping day, not the transit: a window starting after the shipping
day never changes the estimate, per warehouse. labelWarehouseId is the first warehouse
by ascending order with available > 0; it labels the stock source, not the speed.
IMPORTANT: the min() runs over the resulting shipping dates, not over delivery_days.
With a mix of vacation_affected warehouses the faster warehouse can be the slower one
on paper (see Warehouse::$vacationAffected).
Table of Contents
Constants
- DECIMAL_SCALE : int = 3
- ITEMS_TABLE : string = 'cms_mod_store_stock_items'
- LEVELS_TABLE : string = 'cms_mod_store_stock_levels'
- SUPPLIERS_TABLE : string = 'cms_mod_store_suppliers'
- VACATIONS_TABLE : string = 'cms_mod_store_vacations'
- WAREHOUSES_TABLE : string = 'cms_mod_store_warehouses'
Properties
- $em : EntityManagerInterface
- $vacationWindowsMemo : array<int, DateTimeImmutable}>|null
- Request-scoped memo of active vacation windows. It does not depend on item ids or on time ("today" is computed in PHP). `VacationManager` invalidates it through {@see self::resetMemo()} after every write. Stock state (levels, reservations) is never memoized here, because it changes within one request.
Methods
- __construct() : mixed
- findItemByCode() : StockItem|null
- findItemByExternalRef() : StockItem|null
- getAvailability() : ItemAvailability|null
- getAvailabilityBatch() : array<int, ItemAvailability>
- getAvailabilityByExternal() : array<int, ItemAvailability>
- getBatches() : array<int, StockBatch>
- Live batch layers of a card (optionally one warehouse), ordered FEFO with FIFO fallback (expiration ascending with null last, then received, then id ascending).
- getVacationNotice() : array<int, DateTimeImmutable}>
- Returns active and upcoming vacations with an indicative delivery date after each ends.
- resetMemo() : void
- Drops the memoized vacation windows (called by `VacationManager` after a write or delete).
- buildAvailability() : ItemAvailability
- A deactivated stock item is withdrawn from sale: the physical totals and the warehouse rows stay as they are, but nothing is available, there is no delivery estimate and no label warehouse.
- chainPastWindows() : DateTimeImmutable
- Moves `$day` past every window it falls into, re-scanning so that consecutive vacations chain (a shifted day may fall into the next window).
- daysBetween() : int
- decimal() : numeric-string
- loadActiveVacationWindows() : array<int, DateTimeImmutable}>
- Returns active vacation windows (one query). Shared by the delivery algorithm and by the banner helper {@see self::getVacationNotice()}, so there is a single definition of "active vacation" and the two cannot drift apart.
- resolveDelivery() : array{0: ?int, 1: ?int}
- Resolves delivery days and the label warehouse. The vacation shift is computed per warehouse (`vacation_affected`) before `min()`, because a warehouse without the checkbox ships during the vacation too. The minimum is therefore taken over the resulting shipping dates, not over `delivery_days` (a warehouse with a longer lead time but no vacation can deliver sooner than a fast warehouse waiting for the vacation to end).
- resolveShippingDay() : DateTimeImmutable
- Shift today past any active vacation window(s), including consecutive ones.
- resolveVacationAffectedDeliveryDays() : int|null
- Returns `min(delivery_days)` over active warehouses affected by the vacation (`vacation_affected = 1`). Null means no such warehouse exists (all have the checkbox off or are inactive), so the banner shows no date because no warehouse waits for the vacation to end.
- toDay() : DateTimeImmutable|null
- toInt() : int
Constants
DECIMAL_SCALE
private
int
DECIMAL_SCALE
= 3
ITEMS_TABLE
private
string
ITEMS_TABLE
= 'cms_mod_store_stock_items'
LEVELS_TABLE
private
string
LEVELS_TABLE
= 'cms_mod_store_stock_levels'
SUPPLIERS_TABLE
private
string
SUPPLIERS_TABLE
= 'cms_mod_store_suppliers'
VACATIONS_TABLE
private
string
VACATIONS_TABLE
= 'cms_mod_store_vacations'
WAREHOUSES_TABLE
private
string
WAREHOUSES_TABLE
= 'cms_mod_store_warehouses'
Properties
$em read-only
private
EntityManagerInterface
$em
$vacationWindowsMemo
Request-scoped memo of active vacation windows. It does not depend on item ids or on time ("today" is computed in PHP). `VacationManager` invalidates it through {@see self::resetMemo()} after every write. Stock state (levels, reservations) is never memoized here, because it changes within one request.
private
array<int, DateTimeImmutable}>|null
$vacationWindowsMemo
= null
Methods
__construct()
public
__construct(EntityManagerInterface $em) : mixed
Parameters
- $em : EntityManagerInterface
findItemByCode()
public
findItemByCode(string $code) : StockItem|null
Parameters
- $code : string
Return values
StockItem|nullfindItemByExternalRef()
public
findItemByExternalRef(int $externalModule, int $externalId) : StockItem|null
Parameters
- $externalModule : int
- $externalId : int
Return values
StockItem|nullgetAvailability()
public
getAvailability(int $stockItemId) : ItemAvailability|null
Parameters
- $stockItemId : int
Return values
ItemAvailability|nullgetAvailabilityBatch()
public
getAvailabilityBatch(array<string|int, int> $stockItemIds) : array<int, ItemAvailability>
Parameters
- $stockItemIds : array<string|int, int>
Return values
array<int, ItemAvailability> —keyed by stockItemId (only existing items)
getAvailabilityByExternal()
public
getAvailabilityByExternal(int $externalModule, array<string|int, int> $externalIds) : array<int, ItemAvailability>
Parameters
- $externalModule : int
- $externalIds : array<string|int, int>
Return values
array<int, ItemAvailability> —keyed by externalId (only mapped items). On multiple cards mapped to the same externalId, the lowest stock_item_id wins + a warning log.
getBatches()
Live batch layers of a card (optionally one warehouse), ordered FEFO with FIFO fallback (expiration ascending with null last, then received, then id ascending).
public
getBatches(int $stockItemId[, int|null $warehouseId = null ]) : array<int, StockBatch>
Parameters
- $stockItemId : int
- $warehouseId : int|null = null
Return values
array<int, StockBatch>getVacationNotice()
Returns active and upcoming vacations with an indicative delivery date after each ends.
public
getVacationNotice([DateTimeInterface|null $date = null ][, int $daysBefore = 3 ]) : array<int, DateTimeImmutable}>
It is the single source of truth for the vacation banner.
A vacation is returned when its window runs today or starts within $daysBefore days.
Each entry carries:
shipsFrom:end_date + 1, then chained through all further active vacations (a consecutive window moves the date past itself). It uses the same re-scan loop as self::resolveShippingDay(), so the banner and the algorithm cannot disagree.deliveryDays:min(delivery_days)over active warehouses withvacation_affected = 1(only those wait for the vacation to end; null when there is no such warehouse). It is global, not per item, because the banner is global.deliveryEstimate:shipsFrom + deliveryDays(null whendeliveryDaysis null).
It ignores the stock availability of a particular item; that is the job of self::getAvailabilityBatch(). This is an indicative message for the whole website.
Parameters
- $date : DateTimeInterface|null = null
- $daysBefore : int = 3
Return values
array<int, DateTimeImmutable}> —ordered by startDate ascending
resetMemo()
Drops the memoized vacation windows (called by `VacationManager` after a write or delete).
public
resetMemo() : void
buildAvailability()
A deactivated stock item is withdrawn from sale: the physical totals and the warehouse rows stay as they are, but nothing is available, there is no delivery estimate and no label warehouse.
private
buildAvailability(int $stockItemId, array<int, array<string, mixed>> $levelRows, int|null $supplierDeliveryDays, DateTimeImmutable $today, DateTimeImmutable $shiftedDay[, bool $itemActivated = true ]) : ItemAvailability
Parameters
- $stockItemId : int
- $levelRows : array<int, array<string, mixed>>
- $supplierDeliveryDays : int|null
- $today : DateTimeImmutable
- $shiftedDay : DateTimeImmutable
- $itemActivated : bool = true
Return values
ItemAvailabilitychainPastWindows()
Moves `$day` past every window it falls into, re-scanning so that consecutive vacations chain (a shifted day may fall into the next window).
private
chainPastWindows(DateTimeImmutable $day, array<int, DateTimeImmutable}> $windows) : DateTimeImmutable
Parameters
- $day : DateTimeImmutable
- $windows : array<int, DateTimeImmutable}>
Return values
DateTimeImmutabledaysBetween()
private
daysBetween(DateTimeImmutable $from, DateTimeImmutable $to) : int
Parameters
- $from : DateTimeImmutable
- $to : DateTimeImmutable
Return values
intdecimal()
private
decimal(mixed $value) : numeric-string
Parameters
- $value : mixed
Return values
numeric-stringloadActiveVacationWindows()
Returns active vacation windows (one query). Shared by the delivery algorithm and by the banner helper {@see self::getVacationNotice()}, so there is a single definition of "active vacation" and the two cannot drift apart.
private
loadActiveVacationWindows([bool $withId = false ]) : array<int, DateTimeImmutable}>
Parameters
- $withId : bool = false
-
include
vacation_id(banner needs to pair rows with entities)
Return values
array<int, DateTimeImmutable}>resolveDelivery()
Resolves delivery days and the label warehouse. The vacation shift is computed per warehouse (`vacation_affected`) before `min()`, because a warehouse without the checkbox ships during the vacation too. The minimum is therefore taken over the resulting shipping dates, not over `delivery_days` (a warehouse with a longer lead time but no vacation can deliver sooner than a fast warehouse waiting for the vacation to end).
private
resolveDelivery(array<int, array{order: int, warehouseId: int, deliveryDays: int, vacationAffected: bool}> $activeAvailable, int|null $supplierDeliveryDays, DateTimeImmutable $today, DateTimeImmutable $shiftedDay) : array{0: ?int, 1: ?int}
Parameters
- $activeAvailable : array<int, array{order: int, warehouseId: int, deliveryDays: int, vacationAffected: bool}>
- $supplierDeliveryDays : int|null
- $today : DateTimeImmutable
- $shiftedDay : DateTimeImmutable
Return values
array{0: ?int, 1: ?int} —[deliveryDays, labelWarehouseId]
resolveShippingDay()
Shift today past any active vacation window(s), including consecutive ones.
private
resolveShippingDay(array<int, DateTimeImmutable}> $windows) : DateTimeImmutable
Parameters
- $windows : array<int, DateTimeImmutable}>
Return values
DateTimeImmutableresolveVacationAffectedDeliveryDays()
Returns `min(delivery_days)` over active warehouses affected by the vacation (`vacation_affected = 1`). Null means no such warehouse exists (all have the checkbox off or are inactive), so the banner shows no date because no warehouse waits for the vacation to end.
private
resolveVacationAffectedDeliveryDays() : int|null
Return values
int|nulltoDay()
private
toDay(mixed $value) : DateTimeImmutable|null
Parameters
- $value : mixed
Return values
DateTimeImmutable|nulltoInt()
private
toInt(mixed $value) : int
Parameters
- $value : mixed