Skip to main content
The BorisStaticDataClient service provides a resilient data fetching layer with automatic fallback between primary and secondary data sources.

Overview

Located at source/app/Services/BorisStaticDataClient.php, this service:
  • Fetches champion and champion rate data
  • Implements primary/fallback architecture (Boris → Meraki)
  • Validates payload structure before returning data
  • Provides comprehensive error logging
  • Normalizes data formats between sources

Class Definition

namespace App\Services;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;
use Throwable;

class BorisStaticDataClient
{
    // Service implementation
}

Constants

CHAMPIONS_ENDPOINT
string
default:"/lolstaticdata/champions.json"
Boris API endpoint for champion data
CHAMPION_RATES_ENDPOINT
string
default:"/lolstaticdata/championrates.json"
Boris API endpoint for champion rate data
MERAKI_CHAMPIONS_URL
string
Meraki Analytics fallback URL for champion data
MERAKI_CHAMPION_RATES_URL
string
Meraki Analytics fallback URL for champion rates

Public Methods

getChampions()

Fetches champion data with automatic fallback.
public function getChampions(): array
{
    $payload = $this->fetchWithFallback(
        self::CHAMPIONS_ENDPOINT,
        self::MERAKI_CHAMPIONS_URL,
        fn (mixed $payload): bool => $this->isChampionPayload($payload)
    );

    return $this->normalizeChampionPayload($payload);
}
Returns: array - Normalized champion data Throws: RuntimeException - When both Boris and Meraki fail or return invalid data

Usage Example

use App\Services\BorisStaticDataClient;

$client = new BorisStaticDataClient();
$champions = $client->getChampions();

// Returns array of champions
// [
//   ['id' => 1, 'name' => 'Annie', ...],
//   ['id' => 2, 'name' => 'Olaf', ...],
// ]

getChampionRates()

Fetches champion rate/popularity data with automatic fallback.
public function getChampionRates(): array
{
    return $this->fetchWithFallback(
        self::CHAMPION_RATES_ENDPOINT,
        self::MERAKI_CHAMPION_RATES_URL,
        fn (mixed $payload): bool => $this->isChampionRatesPayload($payload)
    );
}
Returns: array - Champion rate data with data key Throws: RuntimeException - When both Boris and Meraki fail or return invalid data

Private Methods

fetchWithFallback()

Core method implementing the fallback pattern.
private function fetchWithFallback(
    string $borisEndpoint, 
    string $merakiUrl, 
    callable $validator
): array
borisEndpoint
string
required
Boris API endpoint path (e.g., /lolstaticdata/champions.json)
merakiUrl
string
required
Full Meraki Analytics URL for fallback
validator
callable
required
Callback to validate payload structure returns bool
Flow:
1

Attempt Boris API

Tries to fetch from Boris API with authentication
2

Validate Boris Response

If successful, validates payload structure using provided validator
3

Fallback to Meraki

If Boris fails or returns invalid data, attempts Meraki Analytics
4

Validate Meraki Response

Validates Meraki payload structure
5

Throw Exception

If both sources fail, throws RuntimeException

fetchFromBoris()

Fetches data from the Boris API with authentication.
private function fetchFromBoris(string $endpoint): mixed
{
    $response = Http::withHeaders([
        'X-API-Key' => (string) config('services.boris.api_key'),
    ])->get($this->borisUrl() . $endpoint);

    return $this->decodeResponse($response, $endpoint, 'boris');
}
Requires BORIS_URL and BORIS_API_KEY environment variables to be configured.

fetchFromMeraki()

Fetches data from Meraki Analytics (no authentication required).
private function fetchFromMeraki(string $url): mixed
{
    $response = Http::get($url);
    return $this->decodeResponse($response, $url, 'meraki');
}

decodeResponse()

Decodes and validates HTTP responses.
private function decodeResponse(
    Response $response, 
    string $endpoint, 
    string $source
): mixed
Validation:
  1. Checks HTTP status is successful (2xx)
  2. Decodes JSON response
  3. Verifies JSON is valid (not null)
Throws: RuntimeException for HTTP errors or invalid JSON

Payload Validators

isChampionPayload()

Validates champion data structure.
private function isChampionPayload(mixed $payload): bool
{
    if (! is_array($payload) || $payload === []) {
        return false;
    }

    if (array_is_list($payload)) {
        return isset($payload[0]['id']);
    }

    $firstChampion = reset($payload);
    return is_array($firstChampion) && isset($firstChampion['id']);
}
Accepts:
  • Numeric array with id key in first element
  • Associative array with champion objects containing id key

isChampionRatesPayload()

Validates champion rates data structure.
private function isChampionRatesPayload(mixed $payload): bool
{
    return is_array($payload) && isset($payload['data']) && is_array($payload['data']);
}
Requires: Top-level data key containing an array

normalizeChampionPayload()

Normalizes champion data to consistent format.
private function normalizeChampionPayload(array $payload): array
{
    if (array_is_list($payload)) {
        return $payload;
    }

    return array_values($payload);
}
Converts: Associative arrays to numeric arrays

Error Handling

The service implements comprehensive error handling with logging:

Warning Logs

Log::warning('Boris static data request failed.', [
    'source' => 'boris',
    'endpoint' => $borisEndpoint,
    'status' => $exception->getCode() ?: null,
    'message' => $exception->getMessage(),
]);

Error Logs

Log::error('Boris and Meraki returned invalid static data payloads.', [
    'source' => 'meraki',
    'endpoint' => $merakiUrl,
    'status' => null,
    'message' => 'Payload shape validation failed for both sources.',
]);
All logs include contextual information (source, endpoint, status, message) for debugging.

Configuration

Required environment variables:
# Boris API Configuration
BORIS_URL=https://boris.heimerdinger.lol
BORIS_API_KEY=your_api_key_here
Configuration file: source/config/services.php
'boris' => [
    'url' => env('BORIS_URL'),
    'api_key' => env('BORIS_API_KEY'),
],

Usage in Commands

The service is typically used in Artisan commands for data seeding:
use App\Services\BorisStaticDataClient;

$client = app(BorisStaticDataClient::class);

try {
    $champions = $client->getChampions();
    $rates = $client->getChampionRates();
    
    // Process and store data
} catch (RuntimeException $e) {
    $this->error('Failed to fetch champion data: ' . $e->getMessage());
}

Best Practices

While the service doesn’t cache internally, callers should cache results to minimize API calls:
$champions = Cache::remember('boris_champions', 3600, function () use ($client) {
    return $client->getChampions();
});
Always wrap calls in try-catch blocks:
try {
    $data = $client->getChampions();
} catch (RuntimeException $e) {
    Log::error('Champion fetch failed', ['error' => $e->getMessage()]);
    // Handle gracefully
}
Monitor logs for frequent fallbacks to Meraki, which may indicate Boris API issues:
  • Warning: Falling back to Meraki static data
  • Error: Boris and Meraki returned invalid static data payloads

Data Sources

Learn about the Boris and Meraki APIs

Champion Model

View the Champion model structure

Configuration

Set up Boris API credentials

Commands

See commands that use this service

Build docs developers (and LLMs) love