PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

MSI – Multi Source Inventory: algorytm doboru źródeł, własny SSA

by Henryk Tews / sobota, 30 maja 2026 / Opublikowano w Magento 2

Multi Source Inventory (MSI) to jeden z największych refaktoringów w historii Magento 2 – wprowadzony w wersji 2.3. Zamiast jednego globalnego stanu magazynowego, produkty mogą mieć stany w wielu źródłach (magazyny, sklepy stacjonarne, dropshipping). Algorytm doboru źródeł (Source Selection Algorithm) decyduje skąd realizować zamówienie. Pokażę architekturę MSI, jak działa domyślny algorytm i jak napisać własny.

Architektura MSI – kluczowe pojęcia

Pojęcie Opis Przykład
Source Fizyczna lokalizacja towaru Magazyn Warszawa, Sklep Kraków
Stock Wirtualna pula źródeł przypisana do kanału sprzedaży Stock „Polska” = WA + KR + GD
Source Item Stan konkretnego SKU w konkretnym źródle SKU-001 w WA: qty=50, status=1
Salable Quantity Dostępna ilość po rezerwacjach 50 – 3 (rezerwacje) = 47
Reservation Tymczasowe zablokowanie towaru przy złożeniu zamówienia -3 przy order placed
SSA Source Selection Algorithm – dobiera źródła do wysyłki Priority, Distance, Custom

Sprawdzanie stanu MSI programatycznie

<?php

declare(strict_types=1);

use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
use Magento\InventorySalesApi\Api\IsProductSalableInterface;
use Magento\InventorySalesApi\Api\IsProductSalableForRequestedQtyInterface;

class StockChecker
{
    public function __construct(
        private GetProductSalableQtyInterface $getSalableQty,
        private IsProductSalableInterface $isProductSalable,
        private IsProductSalableForRequestedQtyInterface $isProductSalableForQty,
    ) {}

    public function check(string $sku, int $stockId = 1): array
    {
        $salableQty = $this->getSalableQty->execute($sku, $stockId);
        $isSalable  = $this->isProductSalable->execute($sku, $stockId);

        return [
            'sku'         => $sku,
            'stock_id'    => $stockId,
            'salable_qty' => $salableQty,
            'is_salable'  => $isSalable,
        ];
    }

    public function canFulfil(string $sku, float $requestedQty, int $stockId = 1): bool
    {
        $result = $this->isProductSalableForQty->execute($sku, $stockId, $requestedQty);
        return $result->isSalable();
    }
}

Source Items – bezpośredni dostęp do stanów źródeł

<?php

declare(strict_types=1);

use Magento\InventoryApi\Api\GetSourceItemsBySkuInterface;
use Magento\InventoryApi\Api\SourceItemsSaveInterface;
use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory;

class SourceItemManager
{
    public function __construct(
        private GetSourceItemsBySkuInterface $getSourceItemsBySku,
        private SourceItemsSaveInterface $sourceItemsSave,
        private SourceItemInterfaceFactory $sourceItemFactory,
    ) {}

    // Pobierz stany ze wszystkich źródeł dla SKU
    public function getBySkus(array $skus): array
    {
        $result = [];
        foreach ($skus as $sku) {
            $sourceItems = $this->getSourceItemsBySku->execute($sku);
            foreach ($sourceItems as $item) {
                $result[$sku][$item->getSourceCode()] = [
                    'qty'    => $item->getQuantity(),
                    'status' => $item->getStatus(),
                ];
            }
        }
        return $result;
    }

    // Ustaw stan w konkretnym źródle
    public function setQty(string $sku, string $sourceCode, float $qty): void
    {
        $sourceItem = $this->sourceItemFactory->create();
        $sourceItem->setSku($sku);
        $sourceItem->setSourceCode($sourceCode);
        $sourceItem->setQuantity($qty);
        $sourceItem->setStatus($qty > 0 ? 1 : 0);

        $this->sourceItemsSave->execute([$sourceItem]);
    }

    // Masowa aktualizacja stanów - efektywna dla importu
    public function batchUpdate(array $updates): void
    {
        // $updates = [['sku' => '...', 'source_code' => '...', 'qty' => 0.0], ...]
        $sourceItems = [];
        foreach ($updates as $update) {
            $item = $this->sourceItemFactory->create();
            $item->setSku($update['sku']);
            $item->setSourceCode($update['source_code']);
            $item->setQuantity($update['qty']);
            $item->setStatus($update['qty'] > 0 ? 1 : 0);
            $sourceItems[] = $item;
        }

        if (!empty($sourceItems)) {
            $this->sourceItemsSave->execute($sourceItems);
        }
    }
}

Custom Source Selection Algorithm

Domyślny SSA „Priority” realizuje zamówienie ze źródeł według priorytetu (liczba w konfiguracji). Custom SSA pozwala na własną logikę – np. dobór najbliższego magazynu do adresu dostawy, minimalizacja kosztów wysyłki, lub preferowanie źródeł z nadwyżką towaru.

<?php

declare(strict_types=1);

namespace Vendor\Inventory\Model\Algorithms;

use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterfaceFactory;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionItemInterfaceFactory;
use Magento\InventorySourceSelectionApi\Model\SourceSelectionInterface;
use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface;

class MinimizeSourcesAlgorithm implements SourceSelectionInterface
{
    public function __construct(
        private GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesOrderedByPriority,
        private SourceSelectionResultInterfaceFactory $selectionResultFactory,
        private SourceSelectionItemInterfaceFactory $selectionItemFactory,
        private \Magento\InventorySourceSelectionApi\Model\GetInStockSourceItemsBySkusAndSortedSource $getInStockItems,
    ) {}

    // Algorytm: realizuj zamówienie z jak najmniejszej liczby źródeł
    // (minimalizuje koszty wysyłki przy wysyłce z wielu lokalizacji)
    public function execute(InventoryRequestInterface $inventoryRequest): SourceSelectionResultInterface
    {
        $stockId      = $inventoryRequest->getStockId();
        $sources      = $this->getSourcesOrderedByPriority->execute($stockId);
        $requestItems = $inventoryRequest->getItems();

        $result       = [];
        $unresolved   = [];

        // Pierwsza faza: sprawdź czy jedno źródło może obsłużyć całe zamówienie
        foreach ($sources as $source) {
            $sourceCode  = $source->getSourceCode();
            $canFulfil   = true;

            foreach ($requestItems as $requestItem) {
                $inStockItems = $this->getInStockItems->execute(
                    [$requestItem->getSku()],
                    [$sourceCode]
                );

                $available = 0.0;
                foreach ($inStockItems as $item) {
                    if ($item->getSourceCode() === $sourceCode) {
                        $available = $item->getQuantity();
                        break;
                    }
                }

                if ($available < $requestItem->getQty()) {
                    $canFulfil = false;
                    break;
                }
            }

            if ($canFulfil) {
                // Jedno źródło wystarczy - użyj go
                foreach ($requestItems as $requestItem) {
                    $result[] = $this->selectionItemFactory->create([
                        'sourceCode'          => $sourceCode,
                        'sku'                 => $requestItem->getSku(),
                        'qtyToDeduct'         => $requestItem->getQty(),
                        'qtyAvailableToDeduct'=> $requestItem->getQty(),
                    ]);
                }

                return $this->selectionResultFactory->create([
                    'sourceSelectionItems' => $result,
                    'shippable'            => true,
                ]);
            }
        }

        // Druga faza: podziel między źródła (fallback do priority SSA)
        return $this->selectionResultFactory->create([
            'sourceSelectionItems' => $result,
            'shippable'            => false,
        ]);
    }
}

Rejestracja Custom SSA w di.xml

<?xml version="1.0"?>
<config>
    <type name="Magento\InventorySourceSelectionApi\Model\Algorithms\Registry">
        <arguments>
            <argument name="algorithms" xsi:type="array">
                <item name="minimize_sources" xsi:type="array">
                    <item name="label" xsi:type="string" translate="true">Minimize Sources</item>
                    <item name="class" xsi:type="string">Vendor\Inventory\Model\Algorithms\MinimizeSourcesAlgorithm</item>
                </item>
            </argument>
        </arguments>
    </type>
</config>

Rezerwacje – jak działają

<?php

// Rezerwacje to mechanizm "soft lock" na salable quantity
// Przy złożeniu zamówienia: reservation qty = -3
// Przy anulowaniu: reservation qty = +3 (kompensacja)
// Przy shipment: reservation usunięta, source item qty zmniejszona

use Magento\InventoryReservationsApi\Model\AppendReservationsInterface;
use Magento\InventoryReservationsApi\Model\ReservationBuilderInterface;

class ReservationManager
{
    public function __construct(
        private AppendReservationsInterface $appendReservations,
        private ReservationBuilderInterface $reservationBuilder,
    ) {}

    public function reserve(string $sku, float $qty, int $stockId, string $metadata): void
    {
        $reservation = $this->reservationBuilder
            ->setSku($sku)
            ->setQuantity(-$qty)  // ujemna = rezerwacja
            ->setStockId($stockId)
            ->setMetadata($metadata)
            ->build();

        $this->appendReservations->execute([$reservation]);
    }

    public function compensate(string $sku, float $qty, int $stockId, string $metadata): void
    {
        $reservation = $this->reservationBuilder
            ->setSku($sku)
            ->setQuantity($qty)   // dodatnia = kompensata
            ->setStockId($stockId)
            ->setMetadata($metadata)
            ->build();

        $this->appendReservations->execute([$reservation]);
    }
}

Podsumowanie

MSI rozdziela fizyczne źródła towaru od wirtualnych puli (Stock) przypisanych do kanałów sprzedaży. Source Selection Algorithm to punkt rozszerzalności gdzie własna logika biznesowa decyduje skąd realizować zamówienie. Rezerwacje zapewniają spójność stanów bez blokowania bazy danych. Custom SSA rejestrowany przez di.xml pojawia się automatycznie w panelu admina przy tworzeniu shipmentu. Następny wpis: zaawansowane enumy w PHP 8.1+ – backed enums, metody, interfejsy.

About Henryk Tews

Co możesz przeczytać następne

Service Contracts – Data Interfaces i Repository Pattern
Hyvä vs Luma benchmark – twarde liczby, LCP 3.5x szybszy, k6 load test, konwersja
GraphQL – własny resolver, schemat, autoryzacja, testowanie w DDEV
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}