setono/gls-webservice-bundle
Symfony bundle that integrates the GLS WebService API into your application, letting you generate shipping labels, book parcels, and track shipments directly from your Symfony project.
Install and register. The package has no Flex recipe and no dependents, so expect a manual setup:
composer require setono/gls-webservice-bundle
Register the bundle explicitly — config/bundles.php on modern Symfony, AppKernel::registerBundles() if you're pinned to an older major. Check the composer.json require block first: the version constraint on symfony/* tells you immediately whether you're in for a fight.
Configure credentials. GLS talks SOAP, so you need your client number plus API username/password (these are not your MyGLS portal login). Put them in .env.local and reference them from the bundle config:
# config/packages/setono_gls_webservice.yaml
setono_gls_webservice:
client_number: '%env(GLS_CLIENT_NUMBER)%'
username: '%env(GLS_USERNAME)%'
password: '%env(GLS_PASSWORD)%'
# wsdl / endpoint / sandbox keys vary by version — verify against the
# bundle's Configuration.php, not the README
Where to look first. In this order:
DependencyInjection/Configuration.php — the authoritative list of config keys. The README is likely stale; the tree is not.Resources/config/services.xml (or services.yaml under config/) — the service IDs you'll inject. Autowiring aliases are probably not defined, so plan on referencing IDs or adding your own aliases.vendor/setono/gls-webservice/src/ — the bundle is thin; the parcel/label value objects and the actual client live in the underlying library. That's where the real API surface is.First use case: print one label. The canonical end-to-end flow is: build a Parcel with sender/receiver/weight → hand it to the label-printing call → receive a base64 PDF plus a GLS parcel number → persist the parcel number → stream the PDF to the user. Get that working for a single hard-coded order before you touch anything else. Everything else (tracking, cancellation, batch printing) is a variation on it.
Wrap the client in your own domain service. Never let controllers or entities touch the GLS client directly. Create App\Shipping\GlsLabelService that takes the underlying client as a constructor argument and exposes intent-revealing methods:
final class GlsLabelService
{
public function __construct(private object $glsClient) {} // service id from services.xml
public function printForOrder(Order $order): PrintedLabel
{
$parcel = $this->parcelFactory->fromOrder($order);
// ...
}
}
This buys you a seam for testing, a place to add logging, and insulation when the bundle is abandoned (see Gotchas).
Order → Parcel factory. The bundle will not know about your Order entity, and you don't want to. Isolate the mapping in a dedicated factory: name, street, house number (GLS often wants these split), postal code, city, ISO-2 country, phone, email, weight in kg, and content description. Keep it a pure function so you can unit-test address normalization without a SOAP call.
Never print labels inside the HTTP request cycle. SOAP + PDF generation is slow and the endpoint is flaky. Dispatch a PrintGlsLabel message via Symfony Messenger, let a worker do the call, and update the order when it completes. Your checkout and admin "Print label" button both become fast.
Persist the parcel number the moment you get it. This is the single most important integration rule. The parcel number is your only handle for tracking and cancellation. Store it on the order and in a gls_parcels table with status, raw response, and timestamps. If the process dies after the SOAP call but before the DB write, you have an orphan label you paid for and can't track.
Tracking as a polling command. GLS has no webhook story here. Write bin/console app:gls:sync-tracking that loads parcels in a non-terminal state, calls the status endpoint, and transitions them. Schedule it with symfony/scheduler or cron at a modest interval (every 30–60 min); don't poll per-request.
Multi-account / multi-depot. If you ship from more than one entity, don't fight the DI container. Build a small registry keyed by channel that holds pre-configured client instances (or clone the client and override credentials per call if the library allows it), and inject the registry, not the client.
Testing. The library object you're calling is likely a concrete class, not an interface. Either define your own interface and adapt it, or use Symfony's service_container test double. Record one real sandbox request/response pair and replay it in fixtures; assert on your domain object, not on SOAP XML.
Logging. Add a GlsClientDecorator that implements the same interface as your adapter and logs request/response envelopes and duration. Without this, debugging a failed label print is guesswork.
This package is effectively unmaintained. Zero stars, zero dependents, and a version history that doesn't match reality means you should pin an exact version in composer.json, commit composer.lock, and budget a day to read the source. Assume you may need to fork or replace it with a thin wrapper over the raw \SoapClient — that's genuinely less code than you think for four endpoints.
The WSDL is a live dependency. ext-soap must be installed, and the WSDL is fetched over HTTP unless you ship a local copy. Point wsdl at a cached local file in production; a GLS outage should not take down your admin panel. Also set connection_timeout and cache_wsdl explicitly.
Base64 before bytes. The label response is base64-encoded — base64_decode() before writing the PDF or streaming it, and check the decoded length for the error string GLS embeds in the body of an otherwise successful-looking response. A "200 OK" with a base64 error message is a common failure mode.
Printing is not idempotent. A retry on a timeout can produce a second, billable label. Distinguish transport-level retries (safe-ish) from application retries (dangerous). Before retrying, check whether the parcel number was already returned, and prefer marking the job manual_review over blindly re-calling.
Address data is stricter than you expect. Country codes get rejected if they're the 3-letter form, postal codes get rejected if they're formatted for the wrong country, and weight above the parcel limit fails at print time, not at construction time. Validate in your factory and surface a clear exception rather than a raw SOAP fault.
Weight is kilograms and gets rounded. Decide once whether you round, and at which step. A 0.996 kg rounding bug becomes a 1.04 kg invoice discrepancy at scale.
No autowiring aliases. If you get Cannot autowire service ... argument $client of type ..., it's not a bug — you need to declare the alias yourself:
services:
Setono\GlsWebservice\ClientInterface:
alias: 'setono_gls_webservice.client'
(Verify the exact ID in services.xml.)
Extension points are the standard Symfony two. Override config under the bundle's root key, and decorate/extend services by redeclaring the service ID. There are almost certainly no events or hooks, so decoration is your only clean entry point.
Debugging recipe. Enable SOAP trace, log __getLastRequest() and __getLastResponse() on exception, and always test against the sandbox endpoint first. Keep a saved "known good" request XML for a single domestic parcel — half of all integration failures are an address field you changed weeks ago.
How can I help you explore Laravel packages today?