juststeveking/companies-house-laravel
A Laravel client for the UK Companies House API, giving you a clean, fluent interface to look up company profiles, officers, filing history, and related records. Configuration is handled through a simple config file, with support for API authentication and typical Laravel conventions.
Read composer.json before you run composer require. The last tagged release is September 2021, so the framework and PHP constraints almost certainly cap at Laravel 8/9 and PHP 7.4/8.0. If your app is on a current Laravel line, decide up front whether you are forking, vendoring, or reimplementing against Http:: — do not paper over it with --ignore-platform-reqs, because you will be the only person who ever notices it break.
composer require juststeveking/companies-house-laravel
php artisan vendor:publish --provider="JustSteveKing\CompaniesHouse\CompaniesHouseServiceProvider"
Publish tags are inconsistent across small packages; if --tag= doesn't resolve, publish by provider (as above) and confirm the config file lands in config/companies-house.php. Set the key in .env — register a free application at the Companies House developer hub to get it — then php artisan config:cache and verify the key still resolves (config-cached apps that read env() directly outside config files are the classic first-hour bug here).
Get the API key, then spend twenty minutes reading src/. The whole package is a thin layer over Laravel's HTTP client: a facade, a config file, an options/request builder, and a handful of endpoint methods. You need to know the exact method names, whether responses are arrays or DTOs, and how errors surface. Don't guess — grep:
grep -rn "class .*Facade" vendor/juststeveking/companies-house-laravel/src
grep -rn "function " vendor/juststeveking/companies-house-laravel/src | head -50
First use case: validate-and-enrich at B2B signup. A user types a company number, you confirm the entity is real and active, and you pre-fill the registered name and address. That single flow exercises search, profile, and registered-address endpoints and is the highest-leverage thing this package enables. Timebox it to half a day and record exact response shapes and status codes — the release notes are one line, so every behaviour claim is a hypothesis until you hit it.
Never let the facade escape the adapter. Put it behind your own interface so controllers, Blade, and jobs are insulated from a 2021 dependency you may replace in an afternoon:
interface CompanyRegistry
{
public function lookup(string $number): CompanyLookup;
}
final class CompaniesHouseRegistry implements CompanyRegistry
{
public function lookup(string $number): CompanyLookup
{
try {
$company = CompaniesHouse::company($number); // confirm name in src
} catch (NotFound $e) { // map to the real exception types
return CompanyLookup::notFound($number);
} catch (ConnectionException|RequestException $e) {
report($e);
return CompanyLookup::unavailable($number);
}
return CompanyLookup::fromApi($number, $company);
}
}
Model the outcome as a small enum-like result, not a boolean. Verified, NotFound, Dissolved, InLiquidation, Unavailable. "Company not found" is a legitimate product answer — collapsing it into the same branch as "API is down" produces the worst possible UX (a real company rejected because the registry had a blip).
Validate the format offline first. Companies House numbers are eight characters: either eight digits, or a two-letter prefix plus six digits (SC Scotland, NI Northern Ireland, OC/SO LLPs, FC overseas). Reject bad formats with a Laravel rule before spending a request, then only call the API for format-valid numbers. Whether the package's validator is offline or API-dependent is a spike question — if it's API-dependent, don't put it in a form request.
Cache by company number with an hours-scale TTL. Profiles change on the order of days, and your signup funnel will hammer the same handful of numbers:
$lookup = Cache::remember(
"companies-house:company:{$number}",
now()->addHours(12),
fn () => $registry->lookup($number),
);
Key on the normalised (uppercased, trimmed) number, and make the TTL configurable so support can flush it when a customer insists their status changed.
Never block the request path on it. A signup or onboarding action should dispatch a job (->afterCommit()) that does the lookup and writes your own companies snapshot table — company_number, name, status, checked_at. You own persistence; the package owns none, ships no migrations, and models nothing in Eloquent. Keeping your own row is what lets you render "last verified 3 days ago" instead of pretending the data is live.
Test with Http::fake(), because there is almost certainly no fake mode in the package. It's built on the Laravel HTTP client, so fakes at the HTTP layer work — but assert on the URL pattern, not just a blanket Http::fake(), or you'll silently pass when the package calls the wrong endpoint:
Http::fake(['api.company-information.service.gov.uk/*' => Http::response([...], 200)]);
Http::fake(['api.company-information.service.gov.uk/*' => Http::response([], 404)]);
Cover 200, 404, 401, 429, and 500. If the package builds URLs itself, a fake that never matches will fail loudly with Http::preventStrayRequests() — enable that in tests.
Pagination is yours. Search, officers, and filing history accept start_index and items_per_page (capped at 100 upstream). If the wrapper doesn't expose them, drop to the underlying request builder or add a thin method; without it any list view is silently truncated at the first page, which is the kind of bug that ships because nobody notices the missing tail.
composer.json (not ^), commit composer.lock, and read the diff of any patch bump. MIT licensing means forking or copying the two service classes into app/Support is a legitimate, low-drama move — treat replacement as an afternoon, not a migration.Http::retry(3, 500, throw: false) in your adapter, or configure the underlying client if the package exposes it. Retrying a 404 is pointless; retrying a 429 without jitter makes the quota problem worse.ConnectionException as Unavailable rather than letting a raw exception 500 the page.base_url . '/search/companies'. A published config value ending in / produces //search and a 404 that looks like a data problem. Check the concat code and trim.data_get() and null-coalesce rather than assuming keys. The resigned_on field added in 2.0.4 is a pass-through from the upstream officer payload — it is not evidence of governance history, and it only appears on officers who have resigned.JustSteveKing\CompaniesHouse appears only in app/Support/CompaniesHouse/. The whole value of the adapter is that removing it later is a one-directory delete plus a container rebind.debug level. You will otherwise be reasoning about an undocumented API through an undocumented wrapper at the same time.How can I help you explore Laravel packages today?