lexik/colissimo-bundle
Symfony bundle for Colissimo shipping integration. Generate labels, print parcel documents, and track deliveries via the Colissimo web services API, with configurable credentials, addresses, and label formats for simple French postal shipping workflows.
Don't install the bundle. lexik/colissimo-bundle is a Symfony 2 bundle — it registers a Bundle class, ships XML/YAML service definitions, and has no Laravel service provider, facade, or config publishing. composer require lexik/colissimo-bundle in a Laravel 10/11 app will at best fail to autoload anything useful and at worst drag EOL Symfony 2 components in. Treat the package as a reference implementation, not a dependency.
What you actually consume is the thing it wraps: lexik/ws-colissimo, a SOAP client for La Poste's WSColiPosteLetterService. Minimal path:
composer require lexik/ws-colissimo
php -m | grep soap # ext-soap must be present in the image
generateLabel([...]), getBordereau(), messages) lives in lexik/ws-colissimo. Everything below maps to that API.config/services.php + .env, not app/config/config.yml:
'colissimo' => [
'wsdl' => env('COLISSIMO_WSDL'),
'login' => env('COLISSIMO_LOGIN'),
'password' => env('COLISSIMO_PASSWORD'),
],
These map to the library constructor args (WSDL URL, WS-Security login, password, SOAP options). Production and validation URLs are different — keep both in .env.example.AppServiceProvider::register() so the SOAP client isn't rebuilt per resolution:
$this->app->singleton(ColissimoClient::class, fn () => new ColissimoClient(
config('services.colissimo.wsdl'),
config('services.colissimo.login'),
config('services.colissimo.password'),
));
php artisan colissimo:smoke) that generates one label for hard-coded sender/addressee with outputFormat: 'PDF', base64-decodes getBordereau(), asserts the bytes start with %PDF, and writes /tmp/test.pdf. Only wire it into orders after that passes against the sandbox.Don't use the vendor client in controllers. Wrap it behind an interface so the SOAP detail never leaks into your domain and so you can swap to a carrier aggregator later (Sendcloud/Boxtal) without touching checkout code.
interface ShippingProvider
{
public function createLabel(Shipment $shipment): Label;
public function track(string $trackingNumber): TrackingStatus;
}
Constructor injection over app(). Laravel makes container lookups easy; resist it. The Symfony bundle's whole value proposition was "inject the service" — preserve that habit.
final class ColissimoShippingProvider implements ShippingProvider
{
public function __construct(private ColissimoClient $client) {}
public function createLabel(Shipment $shipment): Label
{
$response = $this->client->generateLabel([
'outputFormat' => 'PDF',
'letter' => ['service' => 'DOM'],
'parcel' => ['weight' => $shipment->weightKg()],
'sender' => $shipment->sender->toColissimoArray(),
'addressee' => $shipment->recipient->toColissimoArray(),
]);
$this->guardAgainstColissimoErrors($response);
return new Label(base64_decode($response->getBordereau()));
}
}
Keep array-key mapping in value objects. Colissimo requests are large associative arrays with fussy keys (addressee.line1, parcel.weight, letter.service). Put Address::toColissimoArray() / Parcel::toColissimoArray() next to your models so a schema change is one edit, not a grep across controllers.
Run label generation on a queue. The endpoint is slow and flaky. Dispatch a GenerateShippingLabel job from an OrderPaid listener, with $tries = 3 and exponential backoff. Check "does a label already exist for this order?" at the top of the job — retries must be idempotent or you'll burn duplicate bordereaux.
Persist the PDF, don't regenerate it. Decode getBordereau() once, Storage::disk('shipments')->put("labels/{$order->id}.pdf", $pdf), and serve from there. Customers hitting "print label" should never trigger a SOAP call.
Surface Colissimo's business errors. A successful HTTP/SOAP round-trip can still carry error-level entries in the response messages array. Parse them, log the message text verbatim (it's often French and is your only diagnostic signal), and throw a domain exception — never return a half-built label.
Test by binding a fake, not by hitting La Poste.
$this->app->instance(
ColissimoClient::class,
new FakeColissimoClient(base64_encode('%PDF-1.4 ...')),
);
That's the whole reason you wrapped the interface — the vendor class itself has no meaningful test seam.
Log the envelope, redact the credentials. When something breaks, the SOAP request/response body is what you need. Log it at debug level with the WS-Security header stripped.
ext-soap will be missing from your deploy image. Sail, Forge-provisioned boxes, and slim Docker base images routinely omit it. Add docker-php-ext-install soap (plus libxml2-dev) or your first production label fails at runtime, not build time. Gate deploys on php -m | grep -q soap.config:cache integration. Anything the bundle did at container-compile time you now do in AppServiceProvider. Remember php artisan config:cache bakes .env values — use config('services.colissimo.*') at runtime, never env().SoapClient will cache the WSDL locally (default cache_wsdl behaviour) and happily use a stale one after La Poste moves the endpoint. Pin an explicit cache path per environment and clear it in your deploy script; when you see "could not connect" against a URL that works in curl, this is usually why.composer.json PHP constraint before committing. If it's ~5.6|~7.0, you're maintaining a fork — budget for that explicitly rather than discovering it in CI.connection_timeout low. SOAP defaults are generous; a 60s hang inside even a queued job stalls a worker. 10–15s with a queue retry is a better trade than one long block.ShippingProvider boundary clean so switching to an aggregator is a week's work, not a rewrite.How can I help you explore Laravel packages today?