oleksyuk / laravel-gleif-sdk
Laravel SDK for the GLEIF LEI API, built on Saloon
Requires
- php: ^8.4
- illuminate/cache: ^13.0
- illuminate/support: ^13.0
- nesbot/carbon: ^3.0
- saloonphp/cache-plugin: ^3.3
- saloonphp/pagination-plugin: ^2.3
- saloonphp/rate-limit-plugin: ^2.5.3
- saloonphp/saloon: ^4.5
Requires (Dev)
- larastan/larastan: ^3.13.2
- laravel/pint: ^1.32.1
- orchestra/testbench: ^11.3
- pestphp/pest: ^5.3.3
- pestphp/pest-plugin-phpstan: ^5.2.2
- pestphp/pest-plugin-rector: ^5.0.6
- pestphp/pest-plugin-type-coverage: ^5.0.3
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan-deprecation-rules: ^2.0.5
- rector/rector: ^2.8.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-26 14:33:36 UTC
README
laravel-gleif-sdk
Laravel SDK for the GLEIF LEI API, built on Saloon.
Installation
composer require oleksyuk/laravel-gleif-sdk
The service provider and the Gleif facade are auto-discovered. The GLEIF API is public, so there are no credentials to configure.
Usage
use Oleksyuk\Gleif\Facades\Gleif; $record = Gleif::leiRecord()->find('984500A8D612DCC67997')->dto(); echo $record->entityLegalName['name']; // Global Legal Entity Identifier Foundation
Every method returns a Saloon Response: call ->dto() for typed objects from Oleksyuk\Gleif\Data, or ->json() for the raw payload. On an error response ->dto() throws Saloon's RequestException instead of mapping it: e.g. NotFoundException when a related record doesn't exist (an entity without a direct parent) or BadRequestException for a pageSize above 200. An empty, . or .. ID throws InvalidArgumentException before anything is sent. The facade proxies to Oleksyuk\Gleif\GleifConnector, which can be injected instead. It is scoped, so a mock client or header added to it doesn't leak into the next Octane request or queued job.
Covered APIs
| Method | GLEIF API |
|---|---|
leiRecord() |
LEI records, ISINs, level 2 parents/children, related records, modifications |
leiIssuer() |
LEI issuers (LOUs) and their jurisdictions |
vLeiIssuer() |
vLEI issuers |
fuzzyCompletion() |
Fuzzy completions |
autoCompletion() |
Auto completions |
field() |
Fields |
country() |
Countries |
region() |
Regions |
jurisdiction() |
Jurisdictions |
entityLegalForm() |
Entity legal forms (ELF) |
officialOrganizationalRole() |
Official organizational roles (OOR) |
registrationAuthority() |
Registration authorities |
registrationAgent() |
Registration agents |
export() |
Export of 1–300 LEI records in an ExportFormat (CSV, XLSX, JSON, XML) |
Searching
use Oleksyuk\Gleif\Enums\FuzzyCompletionField; use Oleksyuk\Gleif\Enums\RegistrationStatus; $records = Gleif::leiRecord()->all( entityLegalName: 'Global Legal Entity', registrationStatus: RegistrationStatus::Issued, pageSize: 50, )->dto(); $suggestions = Gleif::fuzzyCompletion()->search('gleif', FuzzyCompletionField::EntityLegalName)->dto();
Pagination
all() returns one page (pageSize is at most 200, and GLEIF caps page numbers at 10,000 results). To walk every page lazily, pass a request to paginate(): it follows the links.next GLEIF returns and uses cursor pagination for LEI records, so it isn't capped:
use Oleksyuk\Gleif\Requests\LeiRecords\Lvl1Information\AllLeiRecords; $paginator = Gleif::paginate(new AllLeiRecords(entityLegalAddressCountry: 'UA'))->setPerPageLimit(200); foreach ($paginator->items() as $item) { // $item is an Oleksyuk\Gleif\Data\LeiRecord, as from ->dto() }
Caching
GLEIF publishes a new Golden Copy three times a day (the 00:00, 08:00 and 16:00 UTC copies come out at 02:00, 10:00 and 18:00 UTC), so responses are cached in your Laravel cache until the next publication. The API picks up a new copy hours later, though: a LEI record answer from an older copy than the latest published one (its meta.goldenCopy.publishDate) is cached for 15 minutes only, so the new data shows up soon after the API has it:
gantt
title One day of GLEIF data in UTC, with the API 3 hours behind the files
dateFormat HH:mm
axisFormat %H:%M
section Golden Copy
00 UTC copy → files out : 00:00, 2h
08 UTC copy → files out : 08:00, 2h
16 UTC copy → files out : 16:00, 2h
section GLEIF API
serves the previous 16 UTC copy : done, 00:00, 5h
serves the 00 UTC copy : done, 05:00, 8h
serves the 08 UTC copy : done, 13:00, 8h
serves the 16 UTC copy : done, 21:00, 3h
section Cache of one LEI
cached until the 02 UTC files : active, 00:00, 2h
API behind → asks every 15 min : crit, 02:00, 3h
cached until the 10 UTC files : active, 05:00, 5h
API behind → asks every 15 min : crit, 10:00, 3h
cached until the 18 UTC files : active, 13:00, 5h
API behind → asks every 15 min : crit, 18:00, 3h
cached until the 02 UTC files : active, 21:00, 3h
Loading
GLEIF doesn't say when the API picks up a copy; in practice it takes hours after the files. Caching also covers 404 answers, such as an unknown LEI or an entity without a parent: they don't change in between either. To skip a request that can only answer 404, check the record first: $record->hasDirectParent and $record->hasUltimateParent tell whether GLEIF links a parent record or reports an exception instead. Pick a dedicated store via .env:
GLEIF_CACHE_STORE=redis
It falls back to cache.default. To publish the config file:
php artisan vendor:publish --tag=gleif-config
To skip the cache for one request, send the request yourself:
use Oleksyuk\Gleif\Requests\LeiRecords\Lvl1Information\LeiRecordById; Gleif::send(new LeiRecordById('984500A8D612DCC67997')->disableCaching());
Rate limit
GLEIF allows 60 requests per minute. The connector tracks that limit in the same cache store and sleeps until a slot frees up instead of failing; a 429 from GLEIF is handled the same way, using Retry-After. Cached responses don't count against the limit. Keep that in mind in web requests: a burst of uncached calls can block for up to a minute.
Testing your code
Use Saloon's mocking: no request leaves your app.
use Oleksyuk\Gleif\Requests\LeiRecords\Lvl1Information\LeiRecordById; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; MockClient::global([ LeiRecordById::class => MockResponse::make(['data' => [/* ... */]]), ]);
Development
task lint # PHPStan (max level), Pint, Rector, composer validate task test # Pest task test:coverage # Pest, fails below 100% code coverage; HTML report in build/coverage (needs Xdebug or PCOV) task test:types # Pest, fails below 100% type coverage task fix # auto-fix Rector / Pint issues task contract:update # refresh the GLEIF endpoint snapshot used by ContractTest task fixtures:update # refresh the real GLEIF responses in tests/Fixtures
GLEIF publishes no OpenAPI spec, so tests/Contract/gleif-api.json is a snapshot of the endpoints in its Postman collection: ContractTest checks that every request hits a documented endpoint and every documented endpoint has a request. Fixtures in tests/Fixtures are real GLEIF API resource objects, so the DTO tests check the mapping against what the API actually returns. Both refresh commands need network access; review their diff before committing.