A small, strongly typed PHP client for searching and retrieving economic subjects from the Czech ARES registry.
The package keeps the public API focused: validated query objects go in, immutable DTOs come out, and transport concerns stay behind PSR interfaces. It has no dependency on CakePHP or any other framework but fits naturally into a dependency-injection container.
- Retrieve an economic subject by its eight-digit identification number (IČO).
- Search by identification number, business name, address, legal form, tax office, or CZ-NACE code.
- Navigate typed, result-window-aware pagination.
- Use any PSR-18 HTTP client and PSR-17 request/stream factories.
- Handle validation, transport, API, rate-limit, and mapping failures explicitly.
Note
This project was originally developed in response to a technical assignment as part of a recruitment process.
It is an independent package. It is not affiliated with or endorsed by the Czech Ministry of Finance. Data and service availability are controlled by the official ARES registry.
- Requirements
- Installation
- Quick start
- Retrieve an economic subject
- Search for economic subjects
- Pagination
- Working with results
- ARES ID and identification number
- Configuration
- Error handling
- CakePHP 5 integration
- Public API
- Development
- Design and scope
- License
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| JSON extension | ext-json |
| PSR-18 HTTP client | psr/http-client:^1.0 |
| PSR-17 HTTP factories | psr/http-factory:^1.0 |
| PSR-7 HTTP messages | psr/http-message:^1.1 || ^2.0 |
The package depends on HTTP interfaces, not on a concrete HTTP implementation. Your application therefore remains in control of its HTTP client, timeouts, proxy configuration, logging, and retry policy.
Install the current development branch directly from GitHub:
composer config repositories.ares-php-client vcs https://github.com/jarovarga/ares-php-client
composer require jarovarga/ares-php-client:dev-main guzzlehttp/guzzleThese examples use Guzzle as both the PSR-18 client and the provider of PSR-17 factories. Guzzle is not required by the library itself. You may use any combination that provides:
Psr\Http\Client\ClientInterfacePsr\Http\Message\RequestFactoryInterfacePsr\Http\Message\StreamFactoryInterface
There is deliberately no HTTP-client auto-discovery. Dependencies are passed
to ClientFactory explicitly, which keeps application wiring predictable and easy to replace in tests.
<?php
declare(strict_types=1);
use AresApi\ClientFactory;
use AresApi\Configuration;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
require __DIR__ . '/vendor/autoload.php';
$httpFactory = new HttpFactory();
$client = (new ClientFactory(
new GuzzleClient([
'connect_timeout' => 5.0,
'timeout' => 15.0,
]),
$httpFactory,
$httpFactory,
))->create(new Configuration(
userAgent: 'my-application/1.0',
));The same $client instance can be shared and injected wherever the application needs ARES data:
$economicSubjects = $client->economicSubjects();Use an application-specific user agent in production. It makes requests from your application identifiable without coupling the library to your framework.
Exact lookup accepts a validated IdentificationNumber:
use AresApi\EconomicSubject\ValueObject\IdentificationNumber;
$economicSubject = $client->economicSubjects()->get(
new IdentificationNumber('27074358'),
);
printf(
"%s\n%s\n",
$economicSubject->businessName()
?? 'Business name unavailable',
$economicSubject->registeredOffice()?->formattedAddress()
?? 'Registered office unavailable',
);An identification number (IČO):
- contains exactly eight ASCII digits;
- preserves leading zeroes;
- is never silently trimmed by the value object;
- can be read through
value()or cast tostring.
Malformed string values fail immediately with ValidationException, before an HTTP request is made.
As with any scalar-typed PHP API, use declare(strict_types=1) in calling code
to prevent PHP from coercing integers to strings.
At least one criterion must be supplied:
use AresApi\EconomicSubject\Query\EconomicSubjectSearchQuery;
use AresApi\Pagination\PageRequest;
$result = $client->economicSubjects()->search(
new EconomicSubjectSearchQuery(
businessName: 'Asseco',
addressText: 'Praha',
page: new PageRequest(number: 1, size: 20),
),
);
foreach ($result as $economicSubject) {
printf(
"%s | %s\n",
$economicSubject->identificationNumber()?->value()
?? 'Identification number unavailable',
$economicSubject->businessName()
?? 'Business name unavailable',
);
}The client combines the supplied search criteria into one query sent to ARES.
Supported criteria are:
| Argument | Type | Validation |
|---|---|---|
identificationNumbers |
list<IdentificationNumber> |
At most 100 unique values |
businessName |
?string |
Non-empty UTF-8 text, maximum 2,000 characters |
addressText |
?string |
Non-empty UTF-8 text, maximum 1,500 characters |
legalFormCodes |
list<string> |
Each code contains exactly three digits |
rosLegalFormCodes |
list<string> |
Each code contains exactly three digits |
taxOfficeCodes |
list<string> |
Each code contains exactly three digits |
czNaceCodes |
list<string> |
Each code contains 1–5 non-whitespace characters |
page |
PageRequest |
Defaults to page 1 with 20 items |
A complete query can use all supported filters:
use AresApi\EconomicSubject\Query\EconomicSubjectSearchQuery;
use AresApi\Pagination\PageRequest;
use AresApi\EconomicSubject\ValueObject\IdentificationNumber;
$query = new EconomicSubjectSearchQuery(
identificationNumbers: [
new IdentificationNumber('27074358'),
],
businessName: 'Asseco',
addressText: 'Praha',
legalFormCodes: ['121'],
rosLegalFormCodes: ['121'],
taxOfficeCodes: ['004'],
czNaceCodes: ['620'],
page: new PageRequest(number: 1, size: 25),
);
$result = $client->economicSubjects()->search($query);Text criteria are trimmed. Lists are deduplicated while preserving their first-seen order.
Pagination is one-based:
use AresApi\Pagination\PageRequest;
$page = new PageRequest(number: 2, size: 25);
$page->number(); // 2
$page->size(); // 25
$page->offset(); // 25ARES exposes at most the first 10,000 items of a result set. PageRequest
validates that the requested page starts inside this window:
- page number must be at least
1; - page size must be between
1and10_000; - the requested page offset must be below
10_000; the final page may be partial.
Pagination metadata is returned with every search:
$pageInfo = $result->pageInfo();
$pageInfo->currentPage();
$pageInfo->pageSize();
$pageInfo->totalItems();
$pageInfo->totalPages();
$pageInfo->hasPreviousPage();
$pageInfo->hasNextPage();totalPages() describes the complete match count reported by ARES and can
therefore exceed the accessible result window. Use hasNextPage() when
building navigation; it accounts for both the match count and the 10,000-item limit.
EconomicSubjectSearchResult is both Countable and iterable:
if ($result->isEmpty()) {
echo 'No matching economic subjects.';
}
echo count($result); // Items on this page
echo $result->count(); // Items on this page
echo $result->pageInfo()->totalItems(); // All matches reported by ARES
foreach ($result as $economicSubject) {
// $economicSubject is an AresApi\EconomicSubject\DTO\EconomicSubject
}
$items = $result->items(); // list<EconomicSubject>The main economic-subject accessors are:
| Group | Accessors |
|---|---|
| Identity | aresId(), identificationNumber(), businessName() |
| Registry | legalForm(), taxIdentificationNumber(), taxOfficeCode(), primarySource() |
| Lifecycle | establishedOn(), dissolvedOn(), updatedOn(), hasDissolutionDate() |
| Location | registeredOffice() |
| Activities | czNaceCodes(), czNace2008Codes() |
Economic-subject, address, legal-form, pagination, and result DTOs are immutable. Most economic-subject fields are nullable because ARES does not guarantee that every source register supplies every value.
$address = $economicSubject->registeredOffice();
$formattedAddress = $address?->formattedAddress();
$postalCode = $address?->postalCodeText();
$municipality = $address?->municipalityName();
$countryCode = $address?->countryCode();
$legalFormCode = $economicSubject->legalForm()?->code()
?? $economicSubject->legalForm()?->rosCode();
$establishedOn = $economicSubject->establishedOn()?->format('Y-m-d');Dates are represented as DateTimeImmutable values at midnight UTC.
Activity-code collections are returned as deduplicated lists of strings.
Important
aresId() and identificationNumber() are not interchangeable.
Every mapped economic subject has an ARES record identifier:
$economicSubject->aresId(); // "27074358" or, for example, "ARES_00360478"A Czech identification number (IČO) is a separate, optional value:
$identificationNumber = $economicSubject->identificationNumber()?->value();For many Czech subjects, both identifiers contain the same eight digits. Some
records—particularly records originating from other source registers—only
have an internal prefixed identifier such as ARES_00360478. For those
records, identificationNumber() correctly returns null.
Never display aresId() as a fallback identification number:
// Correct
echo $economicSubject->identificationNumber()?->value()
?? 'Not provided by ARES';
// Incorrect: a prefixed internal ARES ID is not an identification number
echo $economicSubject->identificationNumber()?->value()
?? $economicSubject->aresId();The exact get() operation accepts only IdentificationNumber.
Prefixed ARES IDs can appear in search results but cannot be passed to get().
Configuration controls the base URI, user agent, and additional request headers:
use AresApi\ClientFactory;
use AresApi\Configuration;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
$configuration = new Configuration(
baseUri: Configuration::DEFAULT_BASE_URI,
userAgent: 'my-application/1.0',
headers: [
'X-Correlation-ID' => 'request-123',
],
);
$httpClient = new GuzzleClient([
'connect_timeout' => 5.0,
'timeout' => 15.0,
]);
$httpFactory = new HttpFactory();
$client = (new ClientFactory(
$httpClient,
$httpFactory,
$httpFactory,
))->create($configuration);Defaults:
| Option | Default |
|---|---|
| Base URI | https://ares.gov.cz/ekonomicke-subjekty-v-be/rest |
| User agent | ares-php-client |
| Additional headers | [] |
The base URI must be an absolute HTTP or HTTPS URL without a query or fragment. Changing it is mainly useful for a test server, proxy, or compatible API.
The client owns protocol-sensitive headers and rejects attempts to override them through Configuration:
AcceptContent-LengthContent-TypeHostTransfer-EncodingUser-Agent
Header names are checked case-insensitively. Configure the user agent through
the dedicated userAgent argument.
Timeouts and other transport options belong to the injected HTTP client, as shown in the configuration example above.
All package exceptions extend AresApi\Exception\AresException:
RuntimeException
└── AresException
├── ValidationException
├── RequestException
├── TransportException
├── InvalidResponseException
└── ApiException
├── NotFoundException
└── RateLimitException
| Exception | Meaning |
|---|---|
ValidationException |
Invalid configuration, identifier, query, page, or caller-created DTO |
RequestException |
The PSR request could not be created or its JSON body could not be encoded |
TransportException |
The PSR-18 client could not complete the request |
InvalidResponseException |
ARES returned invalid JSON or an unexpected response structure |
NotFoundException |
ARES returned HTTP 404 |
RateLimitException |
ARES returned HTTP 429 |
ApiException |
ARES returned another non-successful HTTP response |
Catch the most specific failures first:
use AresApi\Exception\ApiException;
use AresApi\Exception\AresException;
use AresApi\Exception\NotFoundException;
use AresApi\Exception\RateLimitException;
use AresApi\EconomicSubject\ValueObject\IdentificationNumber;
$identificationNumber = new IdentificationNumber('27074358');
try {
$economicSubject = $client->economicSubjects()->get($identificationNumber);
} catch (RateLimitException $exception) {
$retryAfter = $exception->retryAfterSeconds();
// Schedule a retry according to your application's policy.
} catch (NotFoundException $exception) {
// No economic subject exists for the requested identification number.
} catch (ApiException $exception) {
$statusCode = $exception->statusCode();
$apiCode = $exception->apiCode();
$apiSubCode = $exception->apiSubCode();
} catch (AresException $exception) {
// Validation, request, transport, or response-mapping failure.
}ApiException also exposes responseBody(). InvalidResponseException may
expose the raw body for decoding failures; it can be null for structural
mapping failures. Treat response bodies as diagnostic data: they can be large
or contain information you do not want to show directly to an end user.
The package does not retry requests automatically. This avoids hidden duplicate traffic and lets the application choose an appropriate retry and backoff strategy.
Register one shared client in src/Application.php. CakePHP can then inject the
interface into controller actions. Commands and application services can use
the same shared instance when their container definitions declare it as an argument.
use AresApi\ClientFactory;
use AresApi\ClientInterface as AresClientInterface;
use AresApi\Configuration;
use Cake\Core\ContainerInterface;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
public function services(ContainerInterface $container): void
{
$container->addShared(
AresClientInterface::class,
static function (): AresClientInterface {
$httpFactory = new HttpFactory();
return (new ClientFactory(
new GuzzleClient([
'connect_timeout' => 5.0,
'timeout' => 15.0,
]),
$httpFactory,
$httpFactory,
))->create(new Configuration(
userAgent: 'my-cakephp-application/1.0',
));
},
);
}Inject the abstraction, not the concrete client:
use AresApi\ClientInterface;
use AresApi\EconomicSubject\Query\EconomicSubjectSearchQuery;
public function search(ClientInterface $ares): void
{
$result = $ares->economicSubjects()->search(
new EconomicSubjectSearchQuery(businessName: 'Asseco'),
);
$this->set(compact('result'));
}This keeps controller code independent of transport construction and makes the ARES boundary straightforward to replace in application tests.
See CakePHP's official dependency-injection documentation for other container wiring options.
The supported public surface consists of:
ClientFactory,ClientInterface, andConfiguration;EconomicSubjectResourceInterface;- economic-subject DTOs, queries, results, and value objects;
- pagination value objects;
- the exception hierarchy under
AresApi\Exception.
Classes and interfaces marked @internal are implementation details and may
change between releases without a backwards-compatibility guarantee. Depend on
the public interfaces and construct clients through ClientFactory.
Install dependencies:
composer installRun the complete quality gate:
composer checkThe command validates composer.json, checks PSR-12 formatting, runs PHPStan at
its strictest level, and executes the test suite. Individual commands are also available:
composer composer:validate
composer cs
composer cs:fix
composer analyse
composer quality
composer testTests use deterministic fixtures and test doubles; they do not require the live ARES service.
The library is intentionally narrow:
ClientInterfaceis the application entry point.EconomicSubjectResourceInterfaceowns economic-subject detail and search operations.- Query and value objects validate caller input before transport.
- PSR-18 and PSR-17 keep infrastructure replaceable.
- Mappers isolate the external response schema from public DTOs.
- Immutable DTOs prevent partially mutated API state.
- Domain-specific exceptions make failures explicit.
Currently supported operations:
| Operation | Client method |
|---|---|
| Retrieve an economic subject by identification number | $client->economicSubjects()->get(...) |
| Search economic subjects with pagination | $client->economicSubjects()->search(...) |
The package currently does not provide:
- automatic retries or backoff;
- caching;
- HTTP-client auto-discovery;
- a raw successful-response payload accessor;
- source-register-specific ARES endpoints.
For the upstream schema and the full set of public ARES endpoints, consult the official ARES REST API documentation.
This project is licensed under the ISC License.