← cd ~/work

case study · open source · PHP library

dvla-api-client/

A PHP client for two DVLA APIs: the Vehicle Enquiry Service, which returns a vehicle’s details from its registration, and KADOE, which returns who kept a vehicle on a given date. It builds on Lendable’s open-source Vehicle Enquiry client; I added the KADOE client and JWT authentication that refreshes itself.

role maintainer extended an open-source client
stack PHP 8.1+ PSR-7 · PSR-18 · value objects
scale 2 DVLA APIs Vehicle Enquiry · KADOE
dates 2025 June 2025

1 · the problem

Two government APIs, two kinds of auth, and secrets that must not end up in a log.

Looking up a vehicle, or who kept it on the date of an event, means two DVLA APIs that authenticate differently: a static API key for one, short-lived tokens from a sign-in call for the other.

Calling them by hand spreads raw arrays, retry-free token handling and credentials through an application. A small library can make the requests typed, the failures explicit and the secrets invisible.

2 · what I built

Typed requests in, typed responses out, auth wrapped around any HTTP client.

The library never chooses an HTTP client for you: it takes any PSR-18 client and wraps it in decorators that add authentication. Lendable’s original gave the Vehicle Enquiry side its shape; I followed the same design for KADOE.

  • A KADOE client Ask by registration number or by chassis/VIN, with the enquirer, reason code, event date and reference the API requires, and get back keeper and address value objects.
  • JWT authentication that refreshes itself A token provider signs in with credentials, caches the token and fetches a new one once it expires; a decorator adds it to every request.
  • Auth as decorators over PSR-18 API-key and JWT authentication wrap whichever PSR-18 client the application already uses, so the library adds no HTTP stack of its own.
  • Secrets that do not leak API keys and tokens are wrapped so they never show up in a var_dump, a stack trace or a log line.
  • Value objects throughout Registration numbers, VINs, dates, MOT and tax status are objects that validate themselves, so a bad value fails at the edge, not inside the API call.
  • Failures you can catch by kind A request that failed, one the API rejected with an error or a message, and a response that could not be decoded are separate exception types.

How a lookup travels through the library

Select any box to see what it does. Your application only ever touches the client and the value objects.

architecture
selected client · scopes [PHP 8.1]

One client per API with a scope for its resource: vehicle details for the Vehicle Enquiry Service, and the keeper at a date for KADOE. It turns value objects into requests and responses back into value objects.

3 · infrastructure

A library has no servers. Its CI is the infrastructure.

Select any box. Every push runs a matrix of six jobs, so the library is proven against the oldest and newest dependencies it allows.

infrastructure
selected 6 jobs [GITHUB ACTIONS]

Two PHP versions times three dependency sets: the lowest versions the constraints allow, the highest, and the lock file. A library has to work for whoever installs it, not just on my machine.

4 · decisions & trade-offs

Every choice had a cost. Here is what it was.

4.1 · Build on what exists

chose
Extend Lendable’s open-source Vehicle Enquiry client
because
It already had the right shape: PSR-18, value objects, typed errors. Adding KADOE in the same style beat writing a second library with a different feel.
cost
Its conventions came with it, so KADOE follows them even where I might have chosen differently.

4.2 · Bring your own HTTP client

chose
Depend on PSR-18 interfaces only
because
Applications already have an HTTP client with their own timeouts and logging. Forcing another one causes version conflicts.
cost
The caller has to supply a client and a request factory, which is a little more set-up.

4.3 · Refresh tokens inside the library

chose
A caching token provider behind the JWT decorator
because
Token expiry is the API’s concern, not the application’s. A worker that runs for hours should never have to think about it.
cost
The library now holds credentials and state, so they are hidden from dumps and the refresh logic is unit-tested.

5 · testing & delivery

Strict analysis on every push, across the versions people actually run.

Every push runs composer validate, a parallel lint, php-cs-fixer and Rector as dry runs, and PHPStan at level 8 with strict rules, on PHP 8.1 and 8.2 against the lowest, highest and locked dependency sets.

The tests mock the PSR-18 client, so they exercise the real request building, auth and decoding without calling the DVLA.

$ vendor/bin/phpstan analyse && vendor/bin/phpunit
PHP versions
8.1 · 8.2
dependency sets
lowest · highest · locked
jobs per push
6
PHPStan
level 8, strict rules
style and refactors
php-cs-fixer · Rector
tests
12, unit and functional

6 · results

$ composer show --tree
DVLA APIs covered
2
HTTP client forced on you
none
runtime dependencies
5, all small
token refresh
automatic
secrets in dumps or logs
hidden
source
public on GitHub

Need a clean client for an awkward API?

$ curl sala.dev/contact -d "re: dvla-api-client"
NORMAL /work/dvla-api-client available UK · remote tony@sala.dev updated
available Start a project →