Wstęp
Ten artykuł ma na celu przedstawienie jednego ze sposobów na stworzenie modułu z przewoźnikami w PrestaShop. Jest skierowany do osób, które mają już pewne doświadczenie w programowaniu.
Poradnik powstał, aby przybliżyć mechanizmy stosowane przez PrestaShop w kwestii wyświetlania dostawców.
Wymagania
Moduł powstał w oparciu o wersję PHP 8.1, ponieważ jest ona zalecana dla PrestaShop 8.1 (która na dzień publikacji tego artykułu jest najnowszą wersją PrestaShop). Ta wersja PHP będzie miała zapewnione wsparcie w nadchodzącej wersji Presty 9.0, więc rozwiązanie przedstawione w artykule powinno pozostać kompatybilne.
Instalacja modułu
Zacznijmy od instalacji modułu.
Przejdź do katalogu modułów w swoim PrestaShop'ie:
cd /path/to/your/prestashop/modules
I sklonuj repozytorium następującym poleceniem:
git clone https://github.com/elektryz/carriermoduleexample.git
Następnie zaloguj się do BackOffice PrestaShop i przejdź do zakładki Moduły.
Wyszukaj moduł wpisując carriermoduleexample, a następnie zainstaluj go.
Możesz oczywiście zrobić to również z użyciem wiersza poleceń z poziomu katalogu głównego Twojego sklepu poleceniem*:
php bin/console prestashop:module install carriermoduleexample
* Jeżeli użycie tej komendy nie powiedzie się, spróbuj użyć aliasu dla Twojej wersji PHP, np. php8.1 lub php81
O usuwaniu i edytowaniu przewoźników słów kilka...
Zanim przejdziemy do dalszej części omawiania modułu, chciałbym zwrócić uwagę na kwestię usuwania przewoźników w PrestaShop.
Być może tego nie wiesz, ale PrestaShop nie kasuje przewoźników z bazy danych. Usunięcie przewoźnika z poziomu
BackOffice nie powoduje usunięcia rekordu z bazy danych (tabela carrier), a jedynie zmianę wartości w kolumnie deleted na 1.
Ręczne usuwanie przewoźnika bezpośrednio przez bazę danych jest możliwe, ale powinno zostać robione z ostrożnością.
Moduł dla celów developerskich będzie pozwalał na takie usuwanie, ale nie jest ono zalecane w przypadku sklepu
produkcyjnego.
Dlaczego Presta zostawia wpisy przewoźników w bazie danych? W przypadku, gdy zostaną utworzone zamówienia z danym przewoźnikiem, ale później z jakiegoś powodu będziemy chcieli pozbyć się tego przewoźnika ze sklepu, to informacje o nim zostaną. Dzięki temu zamówienie nie będzie wskazywało na przewoźnika widmo, tylko wyświetli odpowiednie informacje.
Kwestia edycji przewoźnika przez BackOffice wygląda dość podobnie. Poza ustawieniem flagi deleted na 1,
PrestaShop tworzy nowy wpis przewoźnika w bazie danych, z nowym id_carrier.
System wie, który przewoźnik jest oryginalnym, ponieważ obydwa rekordy w bazie łaczy pole id_reference,
które zawsze powala na jednoznaczną identyfikację przewoźnika, mimo ich różnych wartości id_carrier.
Dalsza część artykułu powinna lepiej wyjaśnić tę kwestię na konkretnym przykładzie.
Konfiguracja modułu
Po zainstalowaniu modułu, przejdź do jego konfiguracji.
Przygotowałem w niej tabelę z listą przewoźników utworzonych przez moduł:

Teraz dowiesz się, jak zdefiniować przewoźników tworzonych przez moduł.
Definiowanie przewoźników
W module umieściłem czterech przewoźników z różnymi ustawieniami.
Zacznijmy od pliku z konfiguracją przewoźników. Otwórz plik:
modules/carriermoduleexample/config/carrier/1_0_0.yml
Znajdziesz w nim listę przewoźników. Każdy z nich jest zdefiniowany w osobnej sekcji:
carriers:
- name: "Module Cost Delivery"
carrier_module_id: "MODULE_COST_DELIVERY"
shipping_external: 1 # [0/1] 1 - use the module to calculate the shipping cost ; 0 - use default shipping cost calculation
- name: "Module Cost Delivery (second)"
carrier_module_id: "MODULE_COST_DELIVERY_SECOND"
shipping_external: 1 # [0/1] 1 - use the module to calculate the shipping cost ; 0 - use default shipping cost calculation
- name: "Standard Delivery - Out of range: use the highest Value"
carrier_module_id: "STANDARD_DELIVERY_HV"
shipping_external: 0
# Below fields are applicable only if shipping_external is set to 0
shipping_method: 2 # [1/2] 1 - by cart items weight ; 2 - by cart items price
range_behavior: 0 # [0/1] if cart is out of range (weight or price): 0 - set the highest price available in this carrier ; 1 - disable the carrier
- name: "Standard Delivery - Out of Range: disable carrier"
carrier_module_id: "STANDARD_DELIVERY_DC"
shipping_external: 0
shipping_method: 2 # [1/2] 1 - by cart items weight ; 2 - by cart items price
range_behavior: 1 # [0/1] if cart is out of range (weight or price): 0 - set the highest price available in this carrier ; 1 - disable the carrier
shipping_handling: 0 # Add handling cost to the shipping cost
W niektórych liniach zostały umieszczone komentarze wyjaśniające, jak dane wartości wpływają na zachowanie w Preście.
Pola name oraz carrier_module_id są wymagane. Drugie z nich jest polem utworzonym przez moduł i musi być unikalne
dla każdego przewoźnika. Zalecam, by tworzyć je wielkimi literami z opcjonalnym oddzieleniem podkreślnikiem.
Pole pozwala określić, który z przewoźników z pliku odpowiada danemu
przewoźnikowi w PrestaShop.
Pozostałe pola są opcjonalne i zależą od tego, jakie ustawienia chcemy przypisać przewoźnikowi.
Mogą to być dowolne właściwości Prestowej klasy CarrierCore, jednak z wykluczeniem pewnych pól. Aby podejrzeć, których
właściwości nie można ustawić, otwórz plik konfiguracyjny modułu:
modules/carriermoduleexample/src/Configuration/ModuleConfiguration.php
i spójrz na stałą CARRIER_FORBIDDEN_FIELDS. Ma to na celu zapobiegnięcie ręcznemu ustawianiu wartości pól, które
powinny być wypełniane automatycznie przez system.
modules/carriermoduleexample/config/logo. Znajdziesz tam plik PNG nazwany zgodnie z
konwencją carrier_module_id.png. Dzięki temu logo zostanie wgrane automatycznie do przewoźnika o podanym
identyfikatorze.
Ustawienia przewoźników
We wspomnianym wcześniej pliku YAML mamy wprowadzone konfiguracje przewoźników. Teraz opiszę je bardziej szczegółowo.
Module Cost Delivery
Przewoźnik z ustawionym polem shipping_external na 1. Oznacza to, że koszt dostawy będzie pobierany bezpośrednio
z modułu, a nie ustawień przewoźnika w Preście.
Module Cost Delivery (second)
Analogicznie do poprzedniego przewoźnika.
Standard Delivery - Out of range: use the highest Value
Przewoźnik korzystający z zakresów (wg łącznej wagi lub kwoty produktów w koszyku), czyli standardowych ustawień przewoźnika w PrestaShop.
W tym przypadku stosowany jest zakres cen (pole shipping_method ma wartość 2).
Jeżeli łączna wartość koszyka przekroczy zakres kwot przewoźnika, zostanie zastosowana najwyższa cena dostawy (pole range_behavior ma wartość 0).
Standard Delivery - Out of Range: disable carrier
Analogicznie do poprzedniego przewoźnika.
Różnicą jest to, że przewoźnik zostanie wyłączony, jeżeli łączna wartość koszyka
przekroczy zakres cen przewoźnika (pole range_behavior ma wartość 1).
Dodatkowo wyłączamy dodawanie kosztów obsługi do kwoty dostawy (pole shipping_handling ma wartość 0, a domyślnie
jest włączone przez Prestę).
Wyświetlanie w koszyku
Przewoźnicy powinni być dostępni w koszyku.

Ustawianie kosztów dostawy
Domyślne ustawienia PrestaShop
W przypadku przewoźników z ustawionym shipping_external na 0, koszt dostawy jest pobierany z ustawień przewoźnika.
Mowa tu o przewoźnikach z nazwą Standard Delivery.
Moduł w takim przypadku tworzy zakresy (kwotowy i wagowy) od 0 do 99999.
Dzieje się to w metodzie insertRanges() klasy:
inIT\CarrierModuleExample\Handler\InsertCarrier
Wejdź na listę przewoźników w Preście i edytuj przewoźnika Standard Delivery:

Nic nie stoi na przeszkodzie, aby zmienić te wartości na inne, jednak nie jest to istotą tego artykułu. Chciałem jedynie zademonstrować, że bez problemu można utworzyć standardowego przewoźnika w systemie z poziomu modułu.
Bardziej będzie nas interesowała kolejna sekcja, czyli...
Pobieranie kwot z modułu
W przypadku przewoźników z ustawionym shipping_external na 1, koszt dostawy jest pobierany z modułu.
Mowa tu o przewoźnikach z nazwą Module Cost Delivery.
Każdy z tych przewoźników musi mieć utworzoną dedykowaną klasę, która pozwoli na indywidualne określenie kwoty dostawy.
Wejdź do katalogu
modules/carriermoduleexample/src/Carrier
W nim znajdziesz klasy ModuleCostDelivery oraz ModuleCostDeliverySecond.
Otwórz te pliki i porównaj (obowiązkowe) metody calculate().
public function calculate(): float|false
{
return $this->cartTotal * 0.1;
}
public function calculate(): float|false
{
return 14.99;
}
Pierwsza metoda ustawia koszt dostawy na 10% wartości produktów w koszyku, a druga zwraca stałą wartość 14.99.
Jest to oczywiście bardzo prosty przykład. W rzeczywistości metoda calculate() może być bardziej skomplikowana i dokonywać
obliczania kosztów na podstawie wielu czynników, takich jak:
- kraju dostawy
- połączenia wartości koszyka i wagi produktów
- konkretnych produktów znajdujących się w koszyku
- grupy klienta
- itp...
W klasach przewoźników w module mamy do dyspozycji obiekt koszyka $this->cart, więc możliwości jest wiele. Możemy nawet przekazać cały
kontekst \Context::getContext() do klasy bazowej i korzystać z niego w klasach przewoźników.
W rzeczywistym przypadku, zamiast zwracać wartość bezpośrednio przez metodę, warto wykonać zapytanie do API z odpowiednimi danymi (np. w formacie JSON) i na ich podstawie wyświetlić uzyskaną w odpowiedzi kwotę dostawy.
Na marginesie - wyjaśnię dlaczego metoda pozwala na zwrócenie wartości false. Ta wartość ma zastosowanie,
gdy chcemy wyłączyć przewoźnika (ustawienie wartości 0 nie wyłączy przewoźnika tylko ustawi darmową dostawę).
A przewoźnika możemy chcieć wyłączyć w różnych przypadkach - np. gdy kraj dostawy nie jest obsługiwany przez
przewoźnika, odpowiedź z API jest pusta/błędna lub wartość/waga produktów jest zbyt duża.
Dla testu możesz spróbować ustawić wartość 0 w pierwszej oraz false w drugiej klasie przewoźnika i odświeżyć koszyk.
Logika wyświetlania kwot z metod głównej klasy modułu
Zacznijmy od zapoznania się z oficjalnymi wytycznymi PrestaShop - tutaj.
Według nich klasa modułu musi dziedziczyć po CarrierModule i implementować metody getOrderShippingCost() oraz getOrderShippingCostExternal().
Teraz otwórz plik:
modules/carriermoduleexample/carriermoduleexample.php
Zjedź na dół pliku - zobaczysz tam następujący kod:
public function getOrderShippingCost($params, $shipping_cost)
{
if (!isset($this->shippingCosts[$this->id_carrier])) {
$this->shippingCosts[$this->id_carrier] = (new CarrierFactory($params, $this->id_carrier))->calculate();
}
return $this->shippingCosts[$this->id_carrier] ?? false;
}
public function getOrderShippingCostExternal($params)
{
return $this->getOrderShippingCost($params, null);
}
getPackageShippingCost, getOrderShippingCost oraz
getOrderShippingCostExternal w pierwszym parametrze posiadają obiekt koszyka.
W kodzie widoczna jest jeszcze zakomentowana metoda getPackageShippingCost().
Nie jest ona potrzebna w naszym przypadku, ale została zachowana gdyż korzystanie z niej może być w pewnych
okolicznościach (patrz - diagram poniżej) również poprawne.
Kod został uproszczony do umieszczenia logiki wewnątrz jednej metody, czyli getOrderShippingCost(), w której tworzymy obiekt przewoźnika modułu
z użyciem klasy CarrierFactory.
Warunek na początku tej metody jest przydatny, ponieważ bez niego ta metoda jest wywoływana kilka razy przy ładowaniu listy przewoźników w sklepie. Brak warunku wiąże się z dwoma minusami:
- wielokrotne powtarzanie wywołań nie jest wydajne
- może skutkować wyświetlaniem błędnych kwot dostawy w koszyku
W klasie głównej modułu mamy dostępną właściwość $this->id_carrier. W jaki sposób jest ustawiana?
Trzeba spojrzeć do klasy Cart. Otwórz plik:
classes/Cart.php
I przejdź do metody getPackageShippingCostFromModule().
Znajduje się tam następujący fragment kodu:
if (property_exists($module, 'id_carrier')) {
$module->id_carrier = $carrier->id;
}
Jeżeli mamy właściwość id_carrier w module, to system ustawi w niej wartość ID przewoźnika.
Dzięki temu wiemy, który przewoźnik w koszyku jest aktualnie przetwarzany przez PrestaShop, co pozwala na wykonanie
kodu przeznaczonego dla konkretnego dostawcy.
Logikę metody Cart::getPackageShippingCostFromModule() zobrazowałem na diagramie przepływu danych:

Tryb DEV modułu
Moduł posiada specjalny tryb DEV. Umożliwia on usuwanie przewoźników tworzonych przez moduł. Powstał po to, aby na etapie tworzenia modułu można było w łatwy sposób masowo usuwać przewoźników po wielokrotnym przeinstalowywaniu modułu.
deleted na 1 -
czyli usuwa przewoźników zgodnie ze sztuką jednocześnie pozostawiając informacje w bazie.
Dokładne informacje o trybie DEV zobaczysz w konfiguracji modułu po jego włączeniu.
Aby go włączyć, przejdź do pliku konfiguracyjnego modułu:
modules/carriermoduleexample/src/Configuration/ModuleConfiguration.php
i ustaw wartość stałej DEV_MODE na true.
public const DEV_MODE = true;
Teraz wejdź w konfigurację modułu - zobaczysz odblokowane nowe opcje:

Odinstaluj moduł i zainstaluj ponownie. Przejdź do konfiguracji, gdzie zobaczysz zmiany w tabeli:

Jak widzisz, stare wpisy przewoźników dalej są dostępne w bazie, ale nie są one już widoczne na stronie przewoźników w
BackOffice. Świeżo utworzeni przewoźnicy przejęli ich carrier_module_id, bo to teraz dla nich będą obliczane koszty
wysyłki (nowe ID oraz Reference).
Teraz przejdź na stronę edycji jednego z tych przewoźników w BackOffice i kliknij "Finish" by wymusić zapisanie zmian:

W moim przypadku edytowałem przewoźnika "Standard Delivery - Out of range: use the highest Value".
Wróć do konfiguracji modułu:

Utworzone zostało nowe ID, natomiast Reference wskazuje na oryginalnego przewoźnika.
Teraz zmień wartość stałej DEV_MODE na false i odśwież stronę konfiguracyjną.

Zobaczysz tylko aktywnych, utworzonych przez moduł przewoźników. Jest to odpowiedni widok dla administratora sklepu i nie pozwala na przypadkowe usunięcie przewoźników.
Wersjonowanie (aktualizacja modułu)
Na potrzeby poradnika utworzyłem w module dodatkowe pliki. Są to:
modules/carriermoduleexample/config/carrier/1_1_0.yml
oraz
modules/carriermoduleexample/upgrade/upgrade-1.1.0.php
Zauważ, że pliki muszą być nazwane zgodnie z konwencją wersjonowania semantycznego.
Pierwszy z nich jest analogiczny do pliku 1_0_0.yml, ale zawiera nowego przewoźnika:
carriers:
- name: "Test upgrade"
carrier_module_id: "CARRIER_FROM_UPGRADE"
shipping_external: 0
shipping_method: 2 # [1/2] 1 - by cart items weight ; 2 - by cart items price
range_behavior: 0 # [0/1] if cart is out of range (weight or price): 0 - set the highest price available in this carrier ; 1 - disable the carrier
Drugi plik zawiera logikę, która zostanie wykonana podczas aktualizacji modułu:
<?php
use inIT\CarrierModuleExample\Helper\VersionHelper;
use inIT\CarrierModuleExample\Installer\Install;
function upgrade_module_1_1_0($object)
{
return (new Install($object, VersionHelper::getInstalledVersion($object)))->install();
}
Ten kod może zostać użyty w dowolnej wersji modułu.
W sytuacji, gdy chcielibyśmy wydać kolejną aktualizację
z nowym przewoźnikem, to analogicznie tworzymy nowe pliki z wersją, np. 1_2_0.yml (pamiętając o unikatowości pola
carrier_module_id) oraz upgrade-1.2.0.php
z metodą upgrade_module_1_2_0.
Pliki są gotowe, więc teraz należy tylko zmienić wersję modułu w pliku głównym.
Otwórz:
modules/carriermoduleexample/carriermoduleexample.php
I ustaw wartość właściwość $this->version na 1.1.0 w konstruktorze modułu:
$this->version = '1.1.0';
Jeżeli w katalogu modułu istnieje plik config.xml - usuń go. Zawiera nieaktualne dane wersji modułu.
Przejdź na stronę modułów w BackOffice. Zobaczysz, że moduł wymaga aktualizacji. Kliknij przycisk "Upgrade".

W konfiguracji modułu pojawi się nowy przewoźnik (obok wcześniej utworzonych). Stało się tak, ponieważ moduł w momencie aktualizacji szuka wszystkich plików YAML z wersjami wyższymi niż wersja sprzed aktualizacji, nie naruszając poprzednich.
Natomiast jeżeli przykładowo udostępnisz swój moduł od razu w wersji 1.2.0, z przewoźnikami zdefiniowanymi w plikach:
1_0_0.yml1_1_0.yml1_2_0.yml
To wszyscy zostaną zainstalowani. Sklep bez żadnej wersji modułu obsłuży wszystkie pliki. A podczas aktualizacji tylko tych, którzy należą do pliku z wersją większą niż wersja obecnie zainstalowana w sklepie.
Podsumowanie
Dziękuję za uwagę i dotrwanie do końca tego (obszernego) artykułu :)
Mam nadzieję, że udało mi się w sposób szczegółowy wyjaśnić proces tworzenia modułu z przewoźnikami oraz logikę działania PrestaShop w tej kwestii.
Jeżeli potrzebujesz pomocy z PrestaShop lub zamierzasz wdrożyć dedykowany moduł do swojego sklepu, zapraszam do kontaktu - z przyjemnością pomogę. Zachęcam do zapoznania się również z innymi, świadczonymi przeze mnie usługami.
Życzę satysfakcjonującej i bezproblemowej pracy z Prestą ;)