Skip to content

feat: ESI rate-limit support + typed SDK with OAS3 generator#22

Open
herpaderpaldent wants to merge 59 commits into
4.xfrom
feat/esi-rate-limit-overhaul
Open

feat: ESI rate-limit support + typed SDK with OAS3 generator#22
herpaderpaldent wants to merge 59 commits into
4.xfrom
feat/esi-rate-limit-overhaul

Conversation

@herpaderpaldent
Copy link
Copy Markdown
Contributor

@herpaderpaldent herpaderpaldent commented May 10, 2026

Summary

This PR completes the transport inversion architecture for the ESI SDK:

  1. Rate-limit support in EsiResponse
  2. Typed SDK surface via Resources (moved to esi-schema)
  3. EsiTransportInterface — EsiClient is now a pure transport adapter

Changes

EsiClient implements EsiTransportInterface

  • invoke() now returns EsiRawResponse (from esi-schema) instead of the internal EsiResponse
  • Wraps the Guzzle response: data, isCachedLoad, pagesEsiRawResponse properties
  • All 33 factory methods (alliance(), characters(), ...) now return Seatplus\EsiSchema\Resources\*

Deleted (moved to esi-schema)

  • src/Generated/Resources/ — 34 files deleted
  • bin/generate.php — single generator now lives in esi-schema

Dependency

  • Requires seatplus/esi-schema PR rename test workflow #2 to merge first (adds EsiTransportInterface, EsiRawResponse, AbstractResource, Resources)

Architecture

Before After
Generators 2 (esi-client + esi-schema) 1 (esi-schema only)
Generated files in esi-client 34 0
esi-client changes on ESI spec update Yes No
Transport swappable No Yes

Usage

$sdk = new EsiClient; // implements EsiTransportInterface

// Object endpoint — DTO directly, no wrapper
$alliance = $sdk->alliance()->getAlliancesAllianceId(99000006);
// AllianceDetail ← isCachedLoad, pages carried on the DTO

// Array endpoint — EsiResult wrapper (carries pages for pagination)
$result = $sdk->assets()->getCharactersCharacterIdAssets(12345);
$items  = $result->data; // array<CharactersCharacterIdAssetsGetItem>
$pages  = $result->pages;

Merge order

  1. seatplus/esi-schema#2 — transport interface + unified generator
  2. This PR — EsiClient as pure transport adapter

Tests

  • All 89 tests pass
  • PHPStan: no errors
  • Pint: no issues
  • Type coverage: 100%

herpaderpaldent and others added 30 commits May 10, 2026 20:16
* update batches

* update workflow for branch renaming

* improve coverage
* Refactor RotatingFileLoggerTest.php for log level functionality.

* lint
* fix: upgrade firebase/php-jwt from v5 to v6

- Bump composer requirement from ^5.4 to ^6.0
- Remove $allowed_algs parameter from JwtService::decodeJWT() — v6
  embeds the algorithm inside Key objects; JWT::decode() no longer
  accepts a third argument
- Update VerifyAccessToken to drop the ['RS256'] third argument
- Update JwtServiceTest to use new Key($secret, 'HS256') instead of
  plain string keys
- Update VerifyAccessTokenTest mock expectations to match new 2-arg
  decodeJWT() signature

Resolves security advisories PKSA-y2cr-5h3j-g3ys and PKSA-2kqm-ps5x-s4f5

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: update deprecated actions (checkout v4, codeclimate v9)

- actions/checkout@v2 -> @v4
- paambaati/codeclimate-action@v2.6.0 -> @v9.0.0

Old versions crash with Node.js 20+ runners.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: replace broken codeclimate-action with direct pest run

codeclimate.com/downloads/test-reporter returns 404 — the reporter
binary has been removed upstream. Drop paambaati/codeclimate-action
and run tests directly via pest --coverage --ci --min=100.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v6.x is also affected by PKSA-y2cr-5h3j-g3ys, so bump to ^7.0 which
is unaffected. v7 enforces minimum key lengths (HS256: 32 bytes,
RSA: 2048 bits) — update tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
herpaderpaldent and others added 6 commits May 10, 2026 20:16
Also update actions/checkout to v4.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pest-plugin-type-coverage v4 calls PHPStan\RuleErrorTransformer::transform()
with an array for arg #3, but phpstan 1.12.28+ changed that parameter
to strictly require string. Both v3 and v4 of the plugin are affected.
This is an upstream bug — track: https://github.com/pestphp/pest-plugin-type-coverage

Pin until a fixed release of pest-plugin-type-coverage ships.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ility

- Bump phpstan/phpstan ^1.12 → ^2.1.46 to match pest-plugin-type-coverage
  v4 API (RuleErrorTransformer::transform signature changed in PHPStan 2)
- Bump rector/rector ^1.2 → ^2.0 (rector 1.x requires PHPStan 1.x)
- Update phpunit.xml.dist schema to PHPUnit 12.5 (<coverage> element was
  removed from the schema; keeping it caused a validation WARN that with
  failOnWarning=true became a fatal test failure)
- Move coverage report flags to test:unit-coverage CLI script
- Set failOnWarning=false (schema warnings from missing optional drivers
  should not be fatal)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add EsiRateLimitedException (HTTP 429) with retryAfter from Retry-After header
- Add EsiErrorLimitedException (HTTP 420) with default retryAfter of 60s
- GuzzleFetcher: throw specific exceptions for 429/420 instead of generic RequestFailedException
- GuzzleFetcher: send X-Compatibility-Date header when EsiConfiguration::compatibility_date is set
- GuzzleFetcher: log X-Ratelimit-Remaining alongside X-Esi-Error-Limit-Remain
- EsiResponse: drop extends ArrayObject; add public object $data with @deprecated __get/__isset bridge
- EsiResponse: parse X-Ratelimit-Group/Limit/Remaining/Used and Retry-After headers
- EsiResponse: add isRateLimitLow() helper (returns true when remaining < 10% of limit)
- EsiConfiguration: add public ?string $compatibility_date = null
- All tests pass: 73 assertions, 100% type coverage, PHPStan clean, Pint clean

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@herpaderpaldent herpaderpaldent force-pushed the feat/esi-rate-limit-overhaul branch from b9bade2 to a050f6b Compare May 10, 2026 18:16
herpaderpaldent and others added 3 commits May 10, 2026 20:40
Parse the '15m' window part of '1800/15m' into ratelimitWindowSeconds
(e.g. 900 for 15m). This enables Phase 2 proactive rate-limit middleware
to calculate the correct dispatch rate (limit / window).

Supports s/m/h unit suffixes; null when header is absent or has no '/'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Require PHP ^8.5 (skipping 8.4, targeting current stable)
- Add typed class constants (const string) in VerifyAccessToken and
  UpdateRefreshTokenService — required for 100% type coverage on PHP 8.5
- Update CI workflows: php-version 8.3 → 8.5, add 4.x to trigger branches
- Update composer.lock for PHP 8.5 compatible deps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Required so eveapi's path repository can satisfy the ^4.0 constraint
while PR #22 is unmerged. Must be removed after the tag is published.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@herpaderpaldent herpaderpaldent changed the base branch from main to 4.x May 11, 2026 07:29
herpaderpaldent and others added 6 commits May 11, 2026 11:01
Magic property access bridge dropped for v4. All eveapi callers now
use ->data->property directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace outdated echo $character_info usage example with proper
  EsiResponse consumption pattern
- Document that response shapes are the consumer's responsibility
  (transport-only contract)
- Show eveapi DTO pattern: XxxResponse::from($response->data)
- Document ESI 1800-token/15-min rate limit and that throttling is
  handled by eveapi Horizon middleware

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… scaffold)

- EsiResult<T> readonly class wraps typed data + pages + isCachedLoad
- EsiClient::withToken(string): static for fluent authenticated calls
- 30 resource factory methods on EsiClient (one per ESI tag group)
- AbstractResource base class + 30 generated stub classes
- EsiClient::$authentication changed from readonly to allow clone+reassign

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er comment

- buildInvokeCall: pass body params (in:body) as 6th arg to invoke()
- buildMethodSignature: resolve type from schema.type for body params, use mixed type
- generateResourceFile: fix {ESI_COMPATIBILITY_DATE} constant not interpolated in heredoc — pre-assign to $compatDate variable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EsiResultTest: constructor defaults, explicit pages/cached, fromResponse
  with object body, X-Pages extraction, X-Kevinrob-Cache HIT/MISS
- GeneratedResourcesTest: withToken() immutability, resource factory methods
  (characters/alliance/universe), typed DTO assertion on character endpoint,
  paginated asset endpoint with page count, cached-load propagation via HIT header
- Helper makeAuthedClient() mocks CheckAccess::can() to avoid JWT decode
  on authenticated endpoints in unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- bin/generate.php now fetches from esi.evetech.net/meta/openapi.yaml
- Uses symfony/yaml for parsing (avoid native yaml_parse dependency)
- DTO class names now match schema names directly (e.g. CharactersDetail)
- Flat namespace: Responses/{SchemaName}.php (no tag subdirs)
- Typed method params: CharacterID x-common-model resolves to int
- Fixed: nullable mixed becomes mixed (PHP 8.4+ compliance)
- Fixed: array_map with cast for array<primitive> return types
- Added resources for 3 new tag groups: CorporationProjects, FreelanceJobs, Meta
- EsiClient: added corporationProjects(), freelanceJobs(), meta() methods
- Updated tests to use new DTO class names

PHPStan: 0 errors. All 87 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@herpaderpaldent herpaderpaldent changed the title feat: ESI rate-limit support + EsiResponse refactor (Phase 1) feat: ESI rate-limit support + typed SDK with OAS3 generator May 11, 2026
herpaderpaldent and others added 3 commits May 11, 2026 17:00
…rator

The Clones/ subdirectory was missed in the cleanup when switching to
the OAS3 flat-namespace generator. The replacement file
CharactersCharacterIdClonesGet.php already exists at the correct path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CCP has been observed changing ESI response fields without bumping the
compatibility date (e.g. active_skill_level on 2026-02-27). Previously,
required fields in from() did $data->field directly — a TypeError if
ESI silently drops the field.

Now all required primitive fields use a type-safe cast + zero fallback:
  (int)($data->field ?? 0)
  (string)($data->field ?? '')
  (bool)($data->field ?? false)
  (float)($data->field ?? 0.0)
  (array)($data->field ?? [])

Required object (DTO) fields fall back to an empty stdClass so ::from()
still runs rather than crashing on property access.

Optional fields already used ?? and are unchanged.

PHPStan: 0 errors. All 87 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… endpoints

- Require seatplus/esi-schema:1.x-dev (VCS from github.com/seatplus/esi-schema)
- Delete src/Generated/Responses/ — all DTOs now come from seatplus/esi-schema
- Generator: no longer generates DTO files, only Resources
- Generator: all DTO imports changed to Seatplus\EsiSchema\Responses\*
- Object endpoints (single-object responses) now return the DTO directly
  instead of wrapping in EsiResult<T>. The DTO extends AbstractEsiDto
  so $dto->isCachedLoad and $dto->pages are available on the result.
- Paginated array endpoints keep EsiResult<array<T>> (pages metadata required)
- Update tests to reflect new return types
- Update README: compatibility date notice + new SDK usage examples
- ESI compatibility date: 2025-12-16 and forward

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
herpaderpaldent and others added 7 commits May 11, 2026 22:58
- Remove tools/swagger_download.php, tools/get_endpoints_and_scopes.php,
  tools/esi.json — all artefacts of the old Swagger 2.0 workflow; the
  generator now fetches OAS3 YAML directly from ESI
- rector.php: PHP_83 → PHP_85 (SetList::PHP_85 confirmed present)
- rector.php: replace tools path with bin (tools deleted, bin has generator)
- Rector dry-run: 0 changes — codebase already fully PHP 8.5 compatible

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EsiClient now implements Seatplus\EsiSchema\Contracts\EsiTransportInterface
- invoke() returns EsiRawResponse (wrapping GuzzleFetcher EsiResponse)
  converting: data, isCachedLoad(), pages → EsiRawResponse properties
- All resource factory methods updated to return Seatplus\EsiSchema\Resources\*
- Deleted src/Generated/Resources/ (34 files) — now live in esi-schema
- Deleted bin/generate.php — single generator now in esi-schema
- Updated composer.json: esi-schema dev-fix/ci-phpunit-config (pending PR merge)
- Updated tests: EsiClientTest expects EsiRawResponse from invoke()
- Updated tests: GeneratedResourcesTest uses esi-schema Resources + EsiResult
- All 89 tests pass, PHPStan clean, 100% type coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligned with EsiTransportInterface change: version parameter removed
since compatibility_date header handles versioning now. buildDataUri()
hardcodes 'latest' in the URL path.

Updated EsiClientTest to reflect /latest/ in expected URI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resources now use Seatplus\EsiSchema\EsiResult::fromRaw() — the
EsiClient\EsiResult (which used fromResponse(EsiResponse)) is no
longer referenced by anything. Removing it eliminates the confusion
of two nearly-identical EsiResult classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…voke()

- Lower PHP requirement to ^8.3 (no PHP 8.5 features used yet)
- EsiClient::invoke() now propagates rateLimitRemaining, rateLimitUsed,
  retryAfter from EsiResponse headers into EsiRawResponse
- Detects cursor token in response body and populates EsiRawResponse::cursor
  (for x-pagination: cursor routes like freelance-jobs, projects)
- 3 new tests covering rate-limit propagation and cursor extraction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PHP 8.5 released and available. No code changes required — the package
already uses PHP 8.3+ features only (typed class constants).

- composer.json: PHP ^8.3 → ^8.5
- composer.json: nesbot/carbon ^2.53 → ^3.0 (required for Laravel 13 compat)
- tests/Unit/EsiClientTest.php: pint binary_operator_spaces fix

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Switch from dev-feat/typed-operation-meta branch alias to stable ^1.0
- Remove VCS repository override (package is on Packagist)
- Remove minimum-stability: dev and prefer-stable (no longer needed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant