Skip to main content
The SaleController handles HTTP requests for the sale rotation page with feature toggle support and comprehensive error handling.

Overview

Located at source/app/Http/Controllers/SaleController.php, this controller:
  • Fetches live sale data from Boris API
  • Implements feature toggle for maintenance mode
  • Caches results for 8 hours
  • Provides detailed error handling and logging
  • Returns structured champion and skin sale data
  • Gracefully handles API failures with user-friendly errors

Class Definition

namespace App\Http\Controllers;

use RuntimeException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SaleController extends Controller
{
    public function index()
    {
        // Implementation
    }
}

Route

Defined in source/routes/web.php:61:
Route::get('sale-rotation', static fn () => (new SaleController)->index())
    ->name('sales.index');

Method

index()

Displays the current sale rotation for champions and skins.
public function index()
{
    // Feature toggle check
    if (!config('sales.enabled')) {
        return view('sales.index');
    }

    try {
        $sales = Cache::remember('sales_data', 60 * 60 * 8, function () {
            // Fetch from Boris API
            $borisUrl = rtrim((string) config('services.boris.url'), '/');
            $borisApiKey = (string) config('services.boris.api_key');

            $headers = ['X-API-Key' => $borisApiKey];

            $championSalesResponse = Http::withHeaders($headers)->get($borisUrl.'/api/champions/sales');
            $skinSalesResponse = Http::withHeaders($headers)->get($borisUrl.'/api/skins/sales');

            // Validation and transformation
            // ...
        });
    } catch (\Exception $exception) {
        // Error handling
        // ...
    }

    return view('sales.index', ['sales' => $sales]);
}

Return Value

view
Illuminate\View\View
Rendered sales.index Blade template

View Data

sales
array
default:"null"
Sale data array with champion_sales and skin_sales keys (null in maintenance mode)

Feature Toggle

The controller checks a configuration flag before attempting to fetch sales:
if (!config('sales.enabled')) {
    return view('sales.index');
}

Configuration

SALES_ENABLED
boolean
default:"false"
Enable/disable the sale rotation feature in .env
# Enable or disable fetching live Sale Rotation data from the external API.
# Set to true to enable fetching, false to show the maintenance message.
SALES_ENABLED=false
When disabled, the view is rendered without sales data, allowing the template to display a maintenance message.

API Integration

The controller fetches data from two Boris API endpoints:

Endpoints

Champion Sales

GET /api/champions/salesReturns champions currently on sale

Skin Sales

GET /api/skins/salesReturns skins currently on sale

Authentication

$headers = [
    'X-API-Key' => $borisApiKey,
];

$championSalesResponse = Http::withHeaders($headers)->get($borisUrl.'/api/champions/sales');
Both endpoints require the X-API-Key header for authentication using BORIS_API_KEY from configuration.

Response Processing

Expected API Response Format

{
  "items": [
    {
      "id": 1,
      "sale_rp": 487,
      "percent_off": 35
    },
    {
      "id": 53,
      "sale_rp": 395,
      "percent_off": 45
    }
  ]
}

Data Transformation

The controller transforms the API response:
return [
    'champion_sales' => array_map(static fn (array $item): array => [
        'item_id' => $item['id'],
        'rp' => $item['sale_rp'],
        'percent_off' => $item['percent_off'],
    ], $championSales['items']),
    'skin_sales' => array_map(static fn (array $item): array => [
        'item_id' => $item['id'],
        'rp' => $item['sale_rp'],
        'percent_off' => $item['percent_off'],
    ], $skinSales['items']),
];

Output Structure

Error Handling

The controller implements multi-level error handling:

HTTP Status Validation

if (! $championSalesResponse->successful() || ! $skinSalesResponse->successful()) {
    Log::error('Boris sales request failed.', [
        'champions_status' => $championSalesResponse->status(),
        'skins_status' => $skinSalesResponse->status(),
    ]);
    
    throw new RuntimeException('Boris sales request failed.');
}

Payload Structure Validation

if (
    ! isset($championSales['items']) || ! is_array($championSales['items']) ||
    ! isset($skinSales['items']) || ! is_array($skinSales['items'])
) {
    Log::error('Boris sales payload is invalid.');
    throw new RuntimeException('Boris sales payload is invalid.');
}

Exception Handling

catch (\Exception $exception) {
    if (
        $exception->getMessage() === 'Boris sales request failed.' ||
        $exception->getMessage() === 'Boris sales payload is invalid.'
    ) {
        abort(503, 'Sorry, the Sale Rotation is currently under maintenance. Please try again later.');
    }

    Log::error('An error occurred while trying to fetch the Sale Rotation', ['error' => $exception->getMessage()]);
    abort(500, 'Sorry, an error occurred while trying to fetch the Sale Rotation. Please try again later.');
}

HTTP Error Codes

503 Service Unavailable
HTTP Status
Returned when Boris API request fails or returns invalid payload structureMessage: “Sorry, the Sale Rotation is currently under maintenance. Please try again later.”
500 Internal Server Error
HTTP Status
Returned for unexpected exceptionsMessage: “Sorry, an error occurred while trying to fetch the Sale Rotation. Please try again later.”

Caching Strategy

$sales = Cache::remember('sales_data', 60 * 60 * 8, function () {
    // Fetch and process API data
});
Sales data is cached for 8 hours (28,800 seconds) to balance data freshness with API rate limits.

Cache Key

  • Key: sales_data
  • TTL: 8 hours
  • Scope: Global (all users see the same cached data)

Manual Cache Invalidation

To force a refresh:
Cache::forget('sales_data');

Logging

All errors are logged with contextual information:

Request Failure Log

Log::error('Boris sales request failed.', [
    'champions_status' => $championSalesResponse->status(),
    'skins_status' => $skinSalesResponse->status(),
]);

Payload Validation Failure Log

Log::error('Boris sales payload is invalid.');

Generic Error Log

Log::error('An error occurred while trying to fetch the Sale Rotation', [
    'error' => $exception->getMessage()
]);
If Discord logging is configured (LOG_DISCORD_WEBHOOK_URL), these errors will also be sent to Discord.

Configuration Requirements

BORIS_URL
string
required
Base URL for the Boris APIExample: https://boris.heimerdinger.lol
BORIS_API_KEY
string
required
API key for authenticating with Boris API
SALES_ENABLED
boolean
default:"false"
Feature toggle to enable/disable sale fetching

Usage Example

Access the sale rotation page:
GET /sale-rotation
Possible Outcomes:
  1. Success: Sales data displayed (from cache or fresh API call)
  2. Maintenance Mode: Feature disabled message shown
  3. 503 Error: Boris API unavailable or returned invalid data
  4. 500 Error: Unexpected server error

Sales Feature

Learn about the sale rotation feature

Data Sources

Learn about the Boris API integration

Configuration

Set up Boris API credentials

Champion Controller

View champion browsing controller

Build docs developers (and LLMs) love