mirror of
https://github.com/rico-vz/HeimerdingerLoL.git
synced 2026-03-21 21:12:43 +01:00
feat: implement boris
This commit is contained in:
@@ -17,6 +17,8 @@ APP_MAINTENANCE_STORE=database
|
|||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LMI_API_KEY=
|
LMI_API_KEY=
|
||||||
|
BORIS_URL=https://boris.heimerdinger.lol
|
||||||
|
BORIS_API_KEY=
|
||||||
|
|
||||||
APP_STAGING=false
|
APP_STAGING=false
|
||||||
|
|
||||||
|
|||||||
163
app/Services/BorisStaticDataClient.php
Normal file
163
app/Services/BorisStaticDataClient.php
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Http\Client\Response;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BorisStaticDataClient
|
||||||
|
{
|
||||||
|
private const CHAMPIONS_ENDPOINT = '/lolstaticdata/champions.json';
|
||||||
|
|
||||||
|
private const CHAMPION_RATES_ENDPOINT = '/lolstaticdata/championrates.json';
|
||||||
|
|
||||||
|
private const MERAKI_CHAMPIONS_URL = 'https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/champions.json';
|
||||||
|
|
||||||
|
private const MERAKI_CHAMPION_RATES_URL = 'https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/championrates.json';
|
||||||
|
|
||||||
|
public function getChampions(): array
|
||||||
|
{
|
||||||
|
return $this->fetchWithFallback(
|
||||||
|
self::CHAMPIONS_ENDPOINT,
|
||||||
|
self::MERAKI_CHAMPIONS_URL,
|
||||||
|
fn (mixed $payload): bool => $this->isChampionPayload($payload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChampionRates(): array
|
||||||
|
{
|
||||||
|
return $this->fetchWithFallback(
|
||||||
|
self::CHAMPION_RATES_ENDPOINT,
|
||||||
|
self::MERAKI_CHAMPION_RATES_URL,
|
||||||
|
fn (mixed $payload): bool => $this->isChampionRatesPayload($payload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchWithFallback(string $borisEndpoint, string $merakiUrl, callable $validator): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$payload = $this->fetchFromBoris($borisEndpoint);
|
||||||
|
|
||||||
|
if ($validator($payload)) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logInvalidPayload('boris', $borisEndpoint, 'Payload shape validation failed.');
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Log::warning('Boris static data request failed.', [
|
||||||
|
'source' => 'boris',
|
||||||
|
'endpoint' => $borisEndpoint,
|
||||||
|
'status' => $exception->getCode() ?: null,
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Falling back to Meraki static data.', [
|
||||||
|
'source' => 'meraki',
|
||||||
|
'endpoint' => $merakiUrl,
|
||||||
|
'message' => 'Boris request failed or returned invalid payload.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payload = $this->fetchFromMeraki($merakiUrl);
|
||||||
|
|
||||||
|
if ($validator($payload)) {
|
||||||
|
Log::warning('Using Meraki static data fallback.', [
|
||||||
|
'source' => 'meraki',
|
||||||
|
'endpoint' => $merakiUrl,
|
||||||
|
'message' => 'Fallback request succeeded.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logInvalidPayload('meraki', $merakiUrl, 'Payload shape validation failed.');
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Log::error('Meraki static data fallback failed.', [
|
||||||
|
'source' => 'meraki',
|
||||||
|
'endpoint' => $merakiUrl,
|
||||||
|
'status' => $exception->getCode() ?: null,
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
sprintf('Unable to fetch static data from Boris or Meraki for [%s].', $borisEndpoint),
|
||||||
|
0,
|
||||||
|
$exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('Boris and Meraki returned invalid static data payloads.', [
|
||||||
|
'source' => 'meraki',
|
||||||
|
'endpoint' => $merakiUrl,
|
||||||
|
'status' => null,
|
||||||
|
'message' => 'Payload shape validation failed for both sources.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
sprintf('Unable to validate static data payload from Boris or Meraki for [%s].', $borisEndpoint)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchFromMeraki(string $url): mixed
|
||||||
|
{
|
||||||
|
$response = Http::get($url);
|
||||||
|
|
||||||
|
return $this->decodeResponse($response, $url, 'meraki');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeResponse(Response $response, string $endpoint, string $source): mixed
|
||||||
|
{
|
||||||
|
if (! $response->successful()) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
sprintf('%s request failed with status %d.', ucfirst($source), $response->status()),
|
||||||
|
$response->status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $response->json();
|
||||||
|
|
||||||
|
if ($payload === null) {
|
||||||
|
throw new RuntimeException(sprintf('%s returned an invalid JSON response.', ucfirst($source)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isChampionPayload(mixed $payload): bool
|
||||||
|
{
|
||||||
|
return is_array($payload) && array_is_list($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isChampionRatesPayload(mixed $payload): bool
|
||||||
|
{
|
||||||
|
return is_array($payload) && isset($payload['data']) && is_array($payload['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function logInvalidPayload(string $source, string $endpoint, string $message): void
|
||||||
|
{
|
||||||
|
Log::warning('Static data payload validation failed.', [
|
||||||
|
'source' => $source,
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'status' => null,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function borisUrl(): string
|
||||||
|
{
|
||||||
|
return rtrim((string) config('services.boris.url'), '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'mysql'),
|
'default' => env('DB_CONNECTION', 'mysql'),
|
||||||
@@ -25,10 +27,10 @@ return [
|
|||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
'strict' => true,
|
'strict' => true,
|
||||||
'engine' => null,
|
'engine' => null,
|
||||||
'options' => extension_loaded('pdo_mysql') ? [
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
PDO::ATTR_TIMEOUT => 15,
|
PDO::ATTR_TIMEOUT => 15,
|
||||||
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
class_exists(Mysql::class) ? Mysql::ATTR_USE_BUFFERED_QUERY : PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||||
] : [],
|
], static fn ($value) => $value !== null) : [],
|
||||||
'sticky' => true,
|
'sticky' => true,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,4 +13,9 @@ return [
|
|||||||
'api_key' => env('LMI_API_KEY'),
|
'api_key' => env('LMI_API_KEY'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'boris' => [
|
||||||
|
'url' => env('BORIS_URL', 'https://boris.heimerdinger.lol'),
|
||||||
|
'api_key' => env('BORIS_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,11 +7,33 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration {
|
||||||
private function indexExists(string $table, string $indexName): bool
|
private function indexExists(string $table, string $indexName): bool
|
||||||
|
{
|
||||||
|
return match (DB::getDriverName()) {
|
||||||
|
'sqlite' => $this->sqliteIndexExists($table, $indexName),
|
||||||
|
default => $this->mysqlIndexExists($table, $indexName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mysqlIndexExists(string $table, string $indexName): bool
|
||||||
{
|
{
|
||||||
$result = DB::select("SHOW INDEX FROM {$table} WHERE Key_name = ?", [$indexName]);
|
$result = DB::select("SHOW INDEX FROM {$table} WHERE Key_name = ?", [$indexName]);
|
||||||
|
|
||||||
return count($result) > 0;
|
return count($result) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sqliteIndexExists(string $table, string $indexName): bool
|
||||||
|
{
|
||||||
|
$indexes = DB::select("PRAGMA index_list('{$table}')");
|
||||||
|
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
if (($index->name ?? null) === $indexName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('champion_skins', function (Blueprint $table) {
|
Schema::table('champion_skins', function (Blueprint $table) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Database\Seeders;
|
|||||||
|
|
||||||
use App\Models\Champion;
|
use App\Models\Champion;
|
||||||
use App\Models\ChampionRoles;
|
use App\Models\ChampionRoles;
|
||||||
|
use App\Services\BorisStaticDataClient;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -15,8 +16,7 @@ class ChampionRolesSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$roleDataUrl = 'https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/championrates.json';
|
$roleData = app(BorisStaticDataClient::class)->getChampionRates();
|
||||||
$roleData = json_decode(file_get_contents($roleDataUrl), true);
|
|
||||||
$changeCount = 0;
|
$changeCount = 0;
|
||||||
|
|
||||||
foreach ($roleData['data'] as $championId => $roles) {
|
foreach ($roleData['data'] as $championId => $roles) {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Champion;
|
use App\Models\Champion;
|
||||||
|
use App\Services\BorisStaticDataClient;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ChampionSeeder extends Seeder
|
class ChampionSeeder extends Seeder
|
||||||
@@ -15,10 +15,7 @@ class ChampionSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$championData = Http::get('https://static.heimerdinger.lol/champions.json')->json();
|
$championData = app(BorisStaticDataClient::class)->getChampions();
|
||||||
if (!is_array($championData)) {
|
|
||||||
$championData = Http::get('https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/champions.json')->json();
|
|
||||||
}
|
|
||||||
$changeCount = 0;
|
$changeCount = 0;
|
||||||
|
|
||||||
foreach ($championData as $champion) {
|
foreach ($championData as $champion) {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\ChampionSkin;
|
use App\Models\ChampionSkin;
|
||||||
|
use App\Services\BorisStaticDataClient;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ChampionSkinSeeder extends Seeder
|
class ChampionSkinSeeder extends Seeder
|
||||||
@@ -15,10 +15,7 @@ class ChampionSkinSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$championData = Http::get('https://static.heimerdinger.lol/champions.json')->json();
|
$championData = app(BorisStaticDataClient::class)->getChampions();
|
||||||
if (!is_array($championData)) {
|
|
||||||
$championData = Http::get('https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/champions.json')->json();
|
|
||||||
}
|
|
||||||
$changeCount = 0;
|
$changeCount = 0;
|
||||||
|
|
||||||
foreach ($championData as $champion) {
|
foreach ($championData as $champion) {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\SkinChroma;
|
use App\Models\SkinChroma;
|
||||||
|
use App\Services\BorisStaticDataClient;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class SkinChromaSeeder extends Seeder
|
class SkinChromaSeeder extends Seeder
|
||||||
@@ -15,10 +15,7 @@ class SkinChromaSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$championData = Http::get('https://static.heimerdinger.lol/champions.json')->json();
|
$championData = app(BorisStaticDataClient::class)->getChampions();
|
||||||
if (!is_array($championData)) {
|
|
||||||
$championData = Http::get('https://cdn.merakianalytics.com/riot/lol/resources/latest/en-US/champions.json')->json();
|
|
||||||
}
|
|
||||||
$changeCount = 0;
|
$changeCount = 0;
|
||||||
|
|
||||||
foreach ($championData as $champion) {
|
foreach ($championData as $champion) {
|
||||||
|
|||||||
Reference in New Issue
Block a user