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 Analytics fallback URL for champion data
MERAKI_CHAMPION_RATES_URL
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
Boris API endpoint path (e.g., /lolstaticdata/champions.json)
Full Meraki Analytics URL for fallback
Callback to validate payload structure returns bool
Flow:
Attempt Boris API
Tries to fetch from Boris API with authentication
Validate Boris Response
If successful, validates payload structure using provided validator
Fallback to Meraki
If Boris fails or returns invalid data, attempts Meraki Analytics
Validate Meraki Response
Validates Meraki payload structure
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:
- Checks HTTP status is successful (2xx)
- Decodes JSON response
- 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