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
Rendered sales.index Blade template
View Data
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
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
{
"items" : [
{
"id" : 1 ,
"sale_rp" : 487 ,
"percent_off" : 35
},
{
"id" : 53 ,
"sale_rp" : 395 ,
"percent_off" : 45
}
]
}
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
Champion ID from Riot API
Discounted price in Riot Points
Discount percentage (e.g., 35 for 35% off)
Discounted price in Riot Points
Discount percentage (e.g., 25 for 25% off)
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
Returned when Boris API request fails or returns invalid payload structure Message: “Sorry, the Sale Rotation is currently under maintenance. Please try again later.”
500 Internal Server Error
Returned for unexpected exceptions Message: “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
Base URL for the Boris API Example: https://boris.heimerdinger.lol
API key for authenticating with Boris API
Feature toggle to enable/disable sale fetching
Usage Example
Access the sale rotation page:
Possible Outcomes:
Success: Sales data displayed (from cache or fresh API call)
Maintenance Mode: Feature disabled message shown
503 Error: Boris API unavailable or returned invalid data
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