Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eseye Laravel Package

seatplus/eseye

SeatPlus ESI is a Laravel package that integrates EVE Online's ESI API into SeatPlus. It provides authenticated requests, token and scope handling, and convenient access to character, corporation, and alliance data, reducing boilerplate for EVE-aware applications.

View on GitHub
Deep Wiki
Context7

Getting Started

seatplus/eseye is a maintained fork of eveseat/eseye — a framework-agnostic client for EVE Online's ESI (EVE Swagger Interface). The fork's practical differences from upstream are: Monolog ^2.0 (upstream pins ^1.0) and ext-redis support for token/cache storage. If your app already runs Monolog 2.x (i.e. any Laravel 8+ project), this fork is what lets you install eseye without dependency conflicts.

Install and bootstrap

composer require seatplus/eseye
use Seat\Eseye\Configuration;
use Seat\Eseye\Eseye;

Configuration::getInstance()->setArray([
    'client_id'     => env('EVE_CLIENT_ID'),
    'client_secret' => env('EVE_CLIENT_SECRET'),
    'callback_url'  => env('EVE_CALLBACK_URL'),
    'scopes'        => ['esi-markets.read_character_orders.v1'],
    'user_agent'    => 'MyApp/1.0 ([email protected])',
    // fork-specific: use phpredis for token/cache backends
    'redis_host'    => env('REDIS_HOST', '127.0.0.1'),
    'redis_port'    => env('REDIS_PORT', 6379),
]);

First call (public endpoint, no auth):

$esi = new Eseye();
$response = $esi->invoke('get', '/characters/{character_id}/', ['character_id' => 123456789]);

if ($response->isOk()) {
    $character = $response->getBody(); // already json_decode'd
}

Where to read first

  1. src/Eseye.php — the public surface: invoke(), get(), post(), setToken(), refreshToken(), getAuthenticationUrl(), authenticate().
  2. src/Configuration.php — every knob is a magic key on a singleton; this is where you learn the actual config names (they're not discoverable from type hints).
  3. src/Containers/EsiResponse.php — the object every call returns; learn getBody(), getStatusCode(), isOk(), hasError(), and the getHeader() accessors.
  4. src/Contracts/TokenStorageInterface.php and src/TokenStorage/ — how tokens are persisted (file, redis).
  5. src/Exceptions/ — EsiException, BadRequestException, RefreshTokenException, etc. These drive your retry logic.

First real use case: fetch a character's public profile and their asset list. The profile requires no auth; assets require an SSO token and the esi-assets.read_assets.v1 scope. Doing these two in sequence exercises the whole library: config, unauthenticated GET, auth redirect, token handling, paginated GET.

Implementation Patterns

Bind as a singleton in a Laravel service provider

Configuration is a static singleton, so isolate it behind a provider and hydrate it from config/services.php. Register a factory, not a shared instance, if you support multiple characters concurrently.

// AppServiceProvider::register()
$this->app->bind(Eseye::class, function () {
    $cfg = Configuration::getInstance();
    $cfg->setArray([
        'client_id'     => config('services.eve.client_id'),
        'client_secret' => config('services.eve.client_secret'),
        'callback_url'  => config('services.eve.callback'),
        'scopes'        => config('services.eve.scopes'),
        'user_agent'    => config('services.eve.user_agent'),
    ]);
    return new Eseye();
});

Don't singleton() it unless you also setToken() per call — see gotchas.

SSO flow

// Redirect route
return redirect((new Eseye())->getAuthenticationUrl());

// Callback route
$esi = new Eseye();
$token = $esi->authenticate(request('code'));  // array: access_token, refresh_token, expires_in
CharacterToken::updateOrCreate(
    ['character_id' => $token['character_id'] ?? null],
    ['token' => json_encode($token)]
);

authenticate() performs the token exchange; you own persistence. That's deliberate — store the token wherever you like (Eloquent column, Redis, vault).

Custom token storage (the Eloquent pattern)

Most Laravel apps skip the bundled file/redis storages and implement the contract against a model:

class EloquentTokenStorage implements TokenStorageInterface
{
    public function __construct(private int $characterId) {}

    public function getToken(): string
    {
        return CharacterToken::findOrFail($this->characterId)->token;
    }

    public function setToken(string $token, int $expires): void
    {
        CharacterToken::where('character_id', $this->characterId)
            ->update(['token' => $token, 'expires_at' => now()->addSeconds($expires)]);
    }

    // fork also expects an updateToken (refresh-token persistence)
}
$esi = new Eseye();
$esi->setTokenStorage(new EloquentTokenStorage($characterId));
$esi->setToken($esi->getTokenStorage()->getToken());

Implement updateToken() too — ESI rotates refresh tokens on refresh, and silently dropping the new one logs the user out on next refresh.

Refresh-before-call middleware

The single most useful pattern: a wrapper that checks expiry, refreshes if under ~60s remaining, and only then invokes ESI. Expiry lives inside the stored token JSON; parse expires_at yourself rather than relying on the library, because the library's clock check is per-instance and easy to bypass.

public function invoke(string $method, string $path, array $args = []): EsiResponse
{
    $token = $this->parse($this->storage->getToken());
    if ($token['expires_at'] < now()->addMinute()) {
        $this->esi->setToken($this->storage->getToken());
        $this->esi->refreshToken();  // writes via storage->updateToken()
        $this->esi->setToken($this->storage->getToken());
    }
    return $this->esi->invoke($method, $path, $args);
}

Pagination

ESI signals pages via the X-Pages header, not the body. The library exposes headers on the response:

$page = 1;
do {
    $resp = $esi->invoke('get', $path, $args, ['page' => $page]);
    $rows = array_merge($rows, $resp->getBody());
    $pages = (int) ($resp->getHeader('X-Pages')[0] ?? 1);
} while ($page++ < $pages);

Wrap this in a reusable paginate() helper — you'll repeat it for assets, orders, contracts, killmails.

Caching reads, keying per character

Configure the redis cache and set a TTL per endpoint. Key on character id + path, never on path alone, or you'll serve character A's wallet to character B. Endpoints like /markets/prices/ and /universe/names/ are safely shared and are where caching pays off most.

Configuration::getInstance()->set('cache_adapter', 'redis');
Configuration::getInstance()->set('cache_ttl', 3600);

For anything user-scoped, prefer caching in your own repository layer with an explicit key, so you don't depend on the library's key derivation.

Error budget and backoff

ESI returns X-ESI-Error-Limit-Remain and X-ESI-Error-Limit-Reset. Read them off every response; when remain drops below a safety floor (say 10), stop issuing non-essential calls and wait reset seconds. A 420 response means you blew the budget — retry only after the reset window, with jitter. This is a queue-level concern: put ESI polling in jobs with release() / backoff, not in the request cycle.

Logging

The fork expects Monolog 2.x; inject a channel and set it on configuration. Log X-ESI-Error-Limit-Remain alongside status codes — that metric is your early warning in production.

Testing

ESI responses are plain objects you can construct in tests, but the cleanest seam is a thin EsiGateway interface you own, with the eseye adapter behind it. Never let new Eseye() appear in a controller; it makes HTTP faking impossible and leaves the static Configuration polluted between tests.

Gotchas and Tips

  • Configuration is a process-wide singleton. Call Configuration::getInstance()->setArray() in a service provider once, not per-request. For tests, reset it in setUp() or you'll leak scopes between cases.
  • Scopes are baked into the auth URL, not the request. Adding a scope means every user must re-authenticate. Request the union of scopes you'll ever need at first login; ESI does not support incremental consent.
  • Refresh tokens rotate. If you don't persist the new refresh token returned by refreshToken(), the next refresh fails. This is the #1 production outage with this library.
  • The library's internal expiry check is unreliable. It compares against the token you set, which may be stale if another process refreshed it. Check expiry yourself; treat RefreshTokenException as "force full re-auth."
  • getBody() is already decoded. It returns arrays/objects, not a string. Calling json_decode($response->getBody()) is a no-op that will silently break on scalar bodies.
  • invoke() vs get()/post(): the convenience methods don't accept the options array for query params consistently. Use invoke('get', $path, $args, $options) when you need page/query parameters; the named helpers are a thin wrapper and their argument order is easy to get wrong.
  • Path parameters are placeholders, not interpolation. '/characters/{character_id}/' with ['character_id' => 123] — passing the interpolated path and the args produces a double-substituted URL.
  • 420 vs 429. ESI's rate limit is the error limit (420), not a request-per-second limit. Generic Laravel rate limiter middleware won't help; you need header-driven logic.
  • Fork lag. seatplus/eseye is a fork; new ESI endpoints are documented in the OpenAPI spec long before they show up here. When you need something the upstream hasn't added, invoke() with a raw path usually works since it's spec-driven — check src/Configuration.php and the endpoint map before assuming you're blocked.
  • phpredis must be the extension, not Predis. The fork's redis support binds ext-redis classes. Installing predis/predis instead won't satisfy it. Check php -m | grep redis in your container.
  • Monolog 2 pin. This is the fork's whole reason to exist, but it also caps you: if a future package requires Monolog 3, this fork will conflict. Track the fork's activity before making it load-bearing.
  • Namespace is Seat\Eseye, not Eseye — copying snippets from upstream docs or older blog posts will fail autoload until you adjust use statements.
  • Debugging checklist: enable the configured Monolog channel, log the full EsiResponse headers (especially x-esi-error-limit-* and X-Pages), and confirm the user_agent contains a contact email — CCP blocks clients without one, sometimes with unhelpful 403s.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle