deliverymatch / pdk
Requires
- php: >=8.1
- deliverymatch/sdk: ^1.0
- nyholm/psr7: ^1.8
- php-di/php-di: ^7.0
- psr/log: ^1.0.0 || ^2.0.0 || ^3.0.0
Requires (Dev)
- ergebnis/phpstan-rules: ^2.6
- friendsofphp/php-cs-fixer: ^3.67
- phpro/grumphp: ^2.10
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.11
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The DeliveryMatch Plugin Development Kit (PDK) is built for creating easy integrations with PHP E-commerce platforms. This library is a wrapper around the SDK
Requirements
- PHP >= 8.1
- A PSR-17 implementation
- A PSR-18 implementation
- Composer
Getting Started
Install the package using Composer:
composer require deliverymatch/pdk
The PDK needs two things from your platform before it can be used:
- A
Repository: durable storage for checkout state, keyed by a checkout reference. - A
CheckoutContext: tells the PDK which checkout the current request belongs to.
The checkout reference is an opaque string your platform chooses, typically the cart or quote id. It must be stable for the whole lifetime of a checkout and reproducible from server-side code (payment webhooks, order validation hooks, cron jobs), because that is where the PDK has to find the shipment id after the shopper has left the storefront.
Implement the Repository interface
Every Repository method takes the checkout reference as its first argument. Implementations store and read data per reference and must not fall back on cookies, sessions or other request-bound state to decide which checkout a call applies to. A database table is the recommended storage: it is reachable from any request, including the ones the shopper's browser is not making.
CREATE TABLE dm_checkout ( checkout_reference VARCHAR(64) NOT NULL PRIMARY KEY, shipment_id INT NULL, check_id VARCHAR(64) NULL, shipping_options LONGTEXT NULL, updated_at DATETIME NOT NULL );
class MyWebshopDataRepository implements \DeliveryMatch\Pdk\Common\Repository { public function __construct(private readonly \PDO $db) { } public function setShipmentId(string $checkoutReference, int $shipmentId): void { $this->upsert($checkoutReference, ['shipment_id' => $shipmentId]); } public function getShipmentId(string $checkoutReference): ?int { $value = $this->column($checkoutReference, 'shipment_id'); return $value === null ? null : (int) $value; } public function setShippingOptions(string $checkoutReference, array $shippingOptions): void { $this->upsert($checkoutReference, ['shipping_options' => base64_encode(serialize($shippingOptions))]); } public function getShippingOptions(string $checkoutReference): array { $value = $this->column($checkoutReference, 'shipping_options'); return $value === null ? [] : unserialize(base64_decode($value)); } public function setCheckId(string $checkoutReference, string $checkId): void { $this->upsert($checkoutReference, ['check_id' => $checkId]); } public function getCheckId(string $checkoutReference): ?string { return $this->column($checkoutReference, 'check_id'); } public function flush(string $checkoutReference): void { $this->db->prepare('DELETE FROM dm_checkout WHERE checkout_reference = ?')->execute([$checkoutReference]); } private function column(string $checkoutReference, string $column): ?string { $stmt = $this->db->prepare("SELECT $column FROM dm_checkout WHERE checkout_reference = ?"); $stmt->execute([$checkoutReference]); $value = $stmt->fetchColumn(); return $value === false ? null : (string) $value; } private function upsert(string $checkoutReference, array $values): void { $values['updated_at'] = date('Y-m-d H:i:s'); $columns = implode(', ', array_keys($values)); $placeholders = implode(', ', array_fill(0, count($values), '?')); $updates = implode(', ', array_map(fn ($c) => "$c = VALUES($c)", array_keys($values))); $this->db ->prepare("INSERT INTO dm_checkout (checkout_reference, $columns) VALUES (?, $placeholders) ON DUPLICATE KEY UPDATE $updates") ->execute([$checkoutReference, ...array_values($values)]); } }
Two things to keep in mind:
flush()is the platform's cleanup hook. The PDK never flushes on its own; callPdk::flushCheckout($reference)once the shipment id has been linked to the order (see below).- Abandoned checkouts are never flushed, so the table grows without bound. Add a retention job that deletes rows whose
updated_atis older than your longest plausible checkout (a few days is usually plenty).
Implement the CheckoutContext interface
CheckoutContext::currentReference() answers "which checkout does the current request belong to?". Return the same reference you would pass to the Repository for that cart, or null when the request is not attributable to a checkout: payment webhooks, CLI commands, back-office requests, or a cart that has not been persisted yet and therefore has no stable id.
class MyWebshopCheckoutContext implements \DeliveryMatch\Pdk\Common\CheckoutContext { public function currentReference(): ?string { $cart = Storefront::currentCart(); if ($cart === null || empty($cart->id)) { return null; } return (string) $cart->id; } }
When currentReference() returns null, PDK read methods (findShippingOption(), getCachedOptions(), getShipmentId()) return an empty result and log a warning, and PDK write methods (fetchShippingOptions(), setSelectedOption(), addShippingOptionToShipment(), flushCheckout()) throw an InvalidStateException. Server-side code should therefore always pass the reference explicitly.
Create a PDK Bootstrapper
The Bootstrapper provides the PDK's dependency injection container with your Repository and CheckoutContext implementations. After creating the bootstrapper call ::setup() to initialize everything. Both bindings are resolved during setup(), so a missing one fails at bootstrap rather than at the first checkout call.
class MyWebshopPdkBootstrapper extends \DeliveryMatch\Pdk\Common\PdkBootstrapper { protected function getAdditionalConfiguration(): array { return [ \DeliveryMatch\Pdk\Common\Repository::class => \DI\autowire(MyWebshopDataRepository::class), \DeliveryMatch\Pdk\Common\CheckoutContext::class => \DI\autowire(MyWebshopCheckoutContext::class), ]; } }
Interact with the PDK
Once the PDK is successfully bootstrapped you can use the PDK to interact with DeliveryMatch using the \Facade\Pdk.
Check if the API connection is successful:
$isAuthenticated = \DeliveryMatch\Pdk\Facade\Pdk::checkConnection();
In the storefront
During the shopper's checkout the current cart is known, so the checkout reference can be omitted and the PDK resolves it through your CheckoutContext.
Fetch shipping options:
$rates = \DeliveryMatch\Pdk\Facade\Pdk::fetchShippingOptions($request);
Read the options fetched earlier without calling DeliveryMatch again:
$options = \DeliveryMatch\Pdk\Facade\Pdk::getCachedOptions();
Store the shipping option the shopper selected:
\DeliveryMatch\Pdk\Facade\Pdk::setSelectedOption($this->request->checkId);
Look up the selected option (for example to display it in the order summary):
$option = \DeliveryMatch\Pdk\Facade\Pdk::findShippingOption();
Server-side: link the shipment to the order
Do this from a hook that runs when the order is created or validated (order validation, payment webhook), not from the confirmation page: that page is not guaranteed to be visited. Such hooks run outside the shopper's request, so pass the checkout reference explicitly.
$cartId = (string) $order->cartId; // Push the selected shipping option to DeliveryMatch \DeliveryMatch\Pdk\Facade\Pdk::addShippingOptionToShipment($cartId); // Store the DeliveryMatch shipment id with the E-commerce order $order->dm_shipment_id = \DeliveryMatch\Pdk\Facade\Pdk::getShipmentId($cartId); $order->save(); // The checkout is complete: remove its state from the repository \DeliveryMatch\Pdk\Facade\Pdk::flushCheckout($cartId);
Update the shipment to new when the payment is received. You also have the option to update the order number and reference. Not all E-commerce platforms provide an order number before the checkout is completed.
\DeliveryMatch\Pdk\Facade\Pdk::updateShipmentToNew(shipmentId: $order->dm_shipment_id, orderNumber: $order->number);
Upgrading to 2.0
Version 2.0 keys all checkout state by an explicit checkout reference instead of "the current request". Every platform integration has to be updated; there is no compatibility layer. See CHANGELOG.md for the full list of changed signatures.
- Implement
\DeliveryMatch\Pdk\Common\CheckoutContextand bind it in your bootstrapper next toRepository. - Rewrite your
Repositoryagainst storage keyed by$checkoutReference(database table recommended). Cookie or session based implementations cannot satisfy the new contract. - Move shipment-to-order linking into a server-side hook and pass the cart id explicitly to
addShippingOptionToShipment(),getShipmentId()andflushCheckout(). - Replace any code that pulled the
Repositoryout of the container to read the shipment id or flush withPdk::getShipmentId()andPdk::flushCheckout(). - Pin
deliverymatch/pdk:^2.0in the same plugin release that ships the new repository.