wearescytale/companies-house-bundle
Symfony bundle for integrating the UK Companies House API into PHP applications. Provides configuration and services to search companies and retrieve profiles, officers, and filing data, reducing boilerplate around authentication and API responses. Useful for lookup and due-diligence features.
This is a Symfony bundle, not a Laravel package — there is no service provider, no facade, and no config/companies_house.php to publish. Treat it as a thin object you wire up yourself.
composer require wearescytale/companies-house-bundle
.env:
COMPANIES_HOUSE_KEY=your-key-here
Add it to config/services.php:
'companies_house' => ['key' => env('COMPANIES_HOUSE_KEY')],
vendor/wearescytale/companies-house-bundle/:
DependencyInjection/*Extension.php — shows the real config keys the bundle expects.Resources/config/services.yml (or services.php) — tells you the service IDs and constructor arguments.// app/Providers/CompaniesHouseServiceProvider.php
public function register(): void
{
$this->app->singleton(\WeAreScytale\CompaniesHouseBundle\CompaniesHouseClient::class, function () {
return new \WeAreScytale\CompaniesHouseBundle\CompaniesHouseClient(
config('services.companies_house.key')
);
});
}
(Verify the exact FQCN and constructor signature against vendor/ — it will differ from this sketch.)$company = app(CompaniesHouseClient::class)->getCompany('09446231');
Do not assume composer require is enough. Until you've opened the bundle's DI extension, you don't know its config shape.
Wrap it behind your own interface immediately. With 2 stars and 0 dependents, the bundle is a liability. Define an interface and adapt the bundle to it so you can swap it for raw Http::withBasicAuth() later without touching call sites:
interface CompanyRegistry
{
public function profile(string $number): ?CompanyData;
}
Caching decorator. Companies House data changes rarely. Cache profiles for 24h and search results for a few minutes:
public function profile(string $number): ?CompanyData
{
return Cache::remember("ch:profile:{$number}", now()->addDay(), function () use ($number) {
return $this->inner->profile($number);
});
}
Queued enrichment. Officers, filing history, and PSC data are separate endpoints. Fan them out into jobs with throttle middleware rather than chaining synchronous HTTP calls in a request:
public function middleware(): array
{
return [(new ThrottlesRequests(500, 5))->by('companies-house')];
}
Validation rule for company numbers. Normalize before hitting the API — uppercase, strip whitespace. Numbers are 8 characters, optionally prefixed with jurisdiction codes (SC, NI, OC, SO, FC, NC, etc.):
'company_number' => ['required', 'string', 'regex:/^[A-Z]{0,2}\d{6,8}$/'],
Bind a fake in tests. Because the bundle's client is a concrete class, either bind your interface to a fake implementation or use Http::fake() if the bundle uses Laravel-visible Guzzle underneath (it likely uses Symfony HttpClient, so Http::fake() may not intercept it — prefer the interface).
Separate "search" from "lookup". Search costs rate-limit budget and returns unstable ranking; lookups by number are cheap and deterministic. Build UIs around lookup, and reserve search for an explicit "find company" form.
config/packages/companies_house.yaml. You must either feed the client its key manually (above) or instantiate the bundle's own config object — there is no automatic bridge.base64($key . ':'). It is not a bearer token and not a query parameter. Most "401 Unauthorized" reports come from getting this wrong.429s, and repeated abuse can get the key suspended. Use Laravel's RateLimiter or job middleware.symfony/* component versions. If it was published against Symfony 5 and you're on Laravel 11 (Symfony 7 components), composer require may fail or force downgrades. Run composer why-not before committing.404 (return null / a typed NotFound result) from 401 (bad key) and 429 (throttled). Letting a 404 throw an exception makes the common path ugly.registered_office_address fields are individually optional. Validate with DTOs and ?? null rather than assuming keys.$response->getContent().Http::withBasicAuth($key, '') as a fallback. Honestly, for most Laravel apps that is the better first move — the bundle is only worth it if you're already in a Symfony codebase.How can I help you explore Laravel packages today?