Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/Exceptions/SimilarGamesCapExceededException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Exceptions;

use RuntimeException;

class SimilarGamesCapExceededException extends RuntimeException
{
public function __construct(
public readonly int $offendingGameId,
public readonly int $cap,
) {
parent::__construct("Game #{$offendingGameId} would exceed the {$cap}-similar-game cap.");
Comment thread
Jamiras marked this conversation as resolved.
}
}
56 changes: 47 additions & 9 deletions app/Filament/Resources/GameResource/Pages/SimilarGames.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Filament\Resources\GameResource\Pages;

use App\Exceptions\SimilarGamesCapExceededException;
use App\Filament\Actions\ParseIdsFromCsvAction;
use App\Filament\Resources\GameResource;
use App\Filament\Resources\SystemResource;
Expand Down Expand Up @@ -61,7 +62,8 @@ public static function getNavigationItems(array $urlParameters = []): array
{
$item = parent::getNavigationItems($urlParameters)[0];
if (($record = $urlParameters['record'] ?? null) instanceof Game) {
$item->badge((string) $record->similarGamesList->count());
$count = $record->similarGamesList->count();
$item->badge("{$count} / " . LinkSimilarGamesAction::MAX_SIMILAR_GAMES);
}

return [$item];
Expand Down Expand Up @@ -127,6 +129,10 @@ public function table(Table $table): Table
->headerActions([
Actions\Action::make('add')
->label('Add similar games')
->disabled(fn (): bool => $this->isSimilarGamesCapReached())
->tooltip(fn (): ?string => $this->isSimilarGamesCapReached()
? 'This game is at the ' . LinkSimilarGamesAction::MAX_SIMILAR_GAMES . '-similar-game cap. Remove a game before adding more.'
: null)
->schema([
Forms\Components\TextInput::make('game_ids_csv')
->label('Game IDs (CSV)')
Expand Down Expand Up @@ -181,18 +187,34 @@ public function table(Table $table): Table
/** @var Game $game */
$game = $this->getOwnerRecord();

$gameIds = [];
$selectIds = !empty($data['game_ids']) ? array_map('intval', $data['game_ids']) : [];
$csvIds = !empty($data['game_ids_csv'])
? (new ParseIdsFromCsvAction())->execute($data['game_ids_csv'])
: [];
$gameIds = array_values(array_unique(array_merge($selectIds, $csvIds)));

// Handle select field input.
if (!empty($data['game_ids'])) {
$gameIds = $data['game_ids'];
(new LinkSimilarGamesAction())->execute($game, $gameIds);
if (empty($gameIds)) {
return;
}

// Handle CSV input.
if (!empty($data['game_ids_csv'])) {
$gameIds = (new ParseIdsFromCsvAction())->execute($data['game_ids_csv']);
try {
(new LinkSimilarGamesAction())->execute($game, $gameIds);
} catch (SimilarGamesCapExceededException $e) {
if ($e->offendingGameId === $game->id) {
$body = "Adding these games would push this game's similar list over the {$e->cap}-similar-game cap. Remove an entry or add fewer games.";
} else {
$offending = Game::find($e->offendingGameId);
$title = $offending?->title ?? "Game #{$e->offendingGameId}";
$body = "{$title} is already at the {$e->cap}-similar-game cap and cannot be linked.";
}

Notification::make()
->danger()
->title('Cap exceeded')
->body($body)
->send();

return;
}

Notification::make()
Expand All @@ -214,6 +236,7 @@ public function table(Table $table): Table
$game = $this->getOwnerRecord();

(new UnlinkSimilarGamesAction())->execute($game, [$similarGame->id]);
$this->isSimilarGamesCapReached = null;

Notification::make()
->success()
Expand All @@ -237,6 +260,7 @@ public function table(Table $table): Table
$game,
$similarGames->pluck('id')->toArray()
);
$this->isSimilarGamesCapReached = null;

$this->deselectAllTableRecords();

Expand All @@ -249,6 +273,20 @@ public function table(Table $table): Table
]);
}

private ?bool $isSimilarGamesCapReached = null;

private function isSimilarGamesCapReached(): bool
{
if ($this->isSimilarGamesCapReached === null) {
/** @var Game $game */
$game = $this->getOwnerRecord();
$similarGamesCount = $game->similarGamesList()->count();
$this->isSimilarGamesCapReached = $similarGamesCount >= LinkSimilarGamesAction::MAX_SIMILAR_GAMES;
}

return $this->isSimilarGamesCapReached;
}

/**
* @param Builder<Game> $query
* @return Builder<Game>
Expand Down
66 changes: 48 additions & 18 deletions app/Platform/Actions/LinkSimilarGamesAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,76 @@

namespace App\Platform\Actions;

use App\Exceptions\SimilarGamesCapExceededException;
use App\Models\Game;
use App\Models\GameSet;
use App\Models\GameSetGame;
use App\Platform\Enums\GameSetType;
use Illuminate\Support\Facades\DB;

class LinkSimilarGamesAction
{
public const int MAX_SIMILAR_GAMES = 6;

public function execute(Game $parentGame, array $gameIdsToLink): void
{
$parentSimilarGamesSet = GameSet::firstOrCreate([
// normalize to a list of ints
$gameIdsToLink = array_values(array_unique(array_map('intval', $gameIdsToLink)));

$parentSimilarGamesSet = GameSet::firstOrNew([
'type' => GameSetType::SimilarGames,
'game_id' => $parentGame->id,
]);

$existingSimilarGames = GameSetGame::query()
->where('game_set_id', $parentSimilarGamesSet->id)
->whereIn('game_id', $gameIdsToLink)
->pluck('game_id')
->toArray();
$existingParentLinkedIds = $parentSimilarGamesSet->exists
? $parentSimilarGamesSet->games()->pluck('games.id')->all()
: [];

$newSetGameIds = array_values(array_diff($gameIdsToLink, $existingParentLinkedIds));

$newSetGameIds = array_diff($gameIdsToLink, $existingSimilarGames);
if (!empty($newSetGameIds)) {
$parentSimilarGamesSet->games()->attach($newSetGameIds);
$existingParentCount = count($existingParentLinkedIds);
if ($existingParentCount + count($newSetGameIds) > self::MAX_SIMILAR_GAMES) {
throw new SimilarGamesCapExceededException($parentGame->id, self::MAX_SIMILAR_GAMES);
}

// Link each game's similar games set to include the parent game.
/** @var GameSet[] $targetSets */
$targetSets = [];
foreach ($gameIdsToLink as $gameId) {
$similarGamesSet = GameSet::firstOrCreate([
$candidateSet = GameSet::firstOrNew([
'type' => GameSetType::SimilarGames,
'game_id' => $gameId,
]);

$isAlreadyAttached = GameSetGame::query()
->where('game_set_id', $similarGamesSet->id)
->where('game_id', $parentGame->id)
->exists();
if ($candidateSet->exists && $candidateSet->games()->where('games.id', $parentGame->id)->exists()) {
continue;
}

if (!$isAlreadyAttached) {
$similarGamesSet->games()->attach($parentGame->id);
$candidateCount = $candidateSet->exists ? $candidateSet->games()->count() : 0;
if ($candidateCount + 1 > self::MAX_SIMILAR_GAMES) {
throw new SimilarGamesCapExceededException($gameId, self::MAX_SIMILAR_GAMES);
}

$targetSets[] = $candidateSet;
}
Comment thread
wescopeland marked this conversation as resolved.

if (empty($newSetGameIds) && empty($targetSets)) {
return;
}

// writes only happen if validation passed
DB::transaction(function () use ($parentGame, $parentSimilarGamesSet, $newSetGameIds, $targetSets): void {
if (!empty($newSetGameIds)) {
if (!$parentSimilarGamesSet->exists) {
$parentSimilarGamesSet->save();
}
$parentSimilarGamesSet->games()->attach($newSetGameIds);
}

foreach ($targetSets as $candidateSet) {
if (!$candidateSet->exists) {
$candidateSet->save();
}
$candidateSet->games()->attach($parentGame->id);
}
});
}
}
Loading