chg: dev: create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Entity\PlayedGame;
|
||||
|
||||
/**
|
||||
* Class BattleShareDto
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class BattleShareDto
|
||||
{
|
||||
public function __construct(
|
||||
public PlayedGame $game,
|
||||
public string $redName,
|
||||
public string $blueName,
|
||||
public ?int $redPts,
|
||||
public ?int $bluePts,
|
||||
public ?string $resign,
|
||||
public ?string $redAvatar,
|
||||
public ?string $blueAvatar,
|
||||
public float $redBonusPoints,
|
||||
public float $blueBonusPoints,
|
||||
public array $redBonusStats,
|
||||
public array $blueBonusStats,
|
||||
public string $ogTitle,
|
||||
public string $ogDesc,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromPlayedGame(PlayedGame $game): self
|
||||
{
|
||||
$redName = $game->red?->getUsername() ?? ($game->redAnon !== null ? 'Anonymous' : 'Guest');
|
||||
$blueName = $game->blue?->getUsername() ?? ($game->blueAnon !== null ? 'Anonymous' : 'Guest');
|
||||
$redPts = $game->redPoints;
|
||||
$bluePts = $game->bluePoints;
|
||||
$resign = $game->resign;
|
||||
|
||||
$summary = self::buildSummary($redName, $blueName, $redPts, $bluePts, $resign);
|
||||
|
||||
return new self(
|
||||
game: $game,
|
||||
redName: $redName,
|
||||
blueName: $blueName,
|
||||
redPts: $redPts,
|
||||
bluePts: $bluePts,
|
||||
resign: $resign,
|
||||
redAvatar: $game->red?->avatarPath,
|
||||
blueAvatar: $game->blue?->avatarPath,
|
||||
redBonusPoints: (float)($game->redBonusPoints ?? 0),
|
||||
blueBonusPoints: (float)($game->blueBonusPoints ?? 0),
|
||||
redBonusStats: $game->redBonusStats ?? [],
|
||||
blueBonusStats: $game->blueBonusStats ?? [],
|
||||
ogTitle: "MineSeeker · $summary",
|
||||
ogDesc: "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
|
||||
);
|
||||
}
|
||||
|
||||
private static function buildSummary(
|
||||
string $redName,
|
||||
string $blueName,
|
||||
?int $redPts,
|
||||
?int $bluePts,
|
||||
?string $resign,
|
||||
): string {
|
||||
if ($resign === 'red') {
|
||||
return "$redName resigned — $blueName wins";
|
||||
}
|
||||
|
||||
if ($resign === 'blue') {
|
||||
return "$blueName resigned — $redName wins";
|
||||
}
|
||||
|
||||
if ($redPts !== null && $bluePts !== null) {
|
||||
if ($redPts > $bluePts) {
|
||||
return "$redName defeated $blueName ($redPts – $bluePts)";
|
||||
}
|
||||
if ($bluePts > $redPts) {
|
||||
return "$blueName defeated $redName ($bluePts – $redPts)";
|
||||
}
|
||||
return "$redName and $blueName drew ($redPts – $bluePts)";
|
||||
}
|
||||
|
||||
return "$redName vs $blueName";
|
||||
}
|
||||
|
||||
public function toTemplateContext(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* Class ProfileChartDataDto
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class ProfileChartDataDto implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public array $months,
|
||||
public array $wins,
|
||||
public array $losses,
|
||||
public array $draws,
|
||||
public int $pieWins,
|
||||
public int $pieLosses,
|
||||
public int $pieDraws,
|
||||
public array $recentGames,
|
||||
) {
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\PlayedGameRepository;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Class ProfileChartDataFactory
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
readonly class ProfileChartDataFactory
|
||||
{
|
||||
public function __construct(private PlayedGameRepository $repo) { }
|
||||
|
||||
public function buildChartData(User $user, int $userId, ProfileStatsDto $stats): ProfileChartDataDto
|
||||
{
|
||||
$monthlyData = $this->buildMonthlyData($user, $userId);
|
||||
|
||||
return new ProfileChartDataDto(
|
||||
months: array_column(array_values($monthlyData), 'label'),
|
||||
wins: array_column(array_values($monthlyData), 'wins'),
|
||||
losses: array_column(array_values($monthlyData), 'losses'),
|
||||
draws: array_column(array_values($monthlyData), 'draws'),
|
||||
pieWins: $stats->wins,
|
||||
pieLosses: $stats->losses,
|
||||
pieDraws: $stats->draws,
|
||||
recentGames: $this->buildRecentGamesSeries($user, $userId),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildMonthlyData(User $user, int $userId): array
|
||||
{
|
||||
$monthlyData = [];
|
||||
|
||||
for ($i = 5; $i >= 0; $i--) {
|
||||
$dt = new DateTime("first day of -$i months midnight");
|
||||
$key = $dt->format('Y-m');
|
||||
$monthlyData[$key] = ['label' => $dt->format('M'), 'wins' => 0, 'losses' => 0, 'draws' => 0];
|
||||
}
|
||||
|
||||
$since = new DateTime('first day of -5 months midnight');
|
||||
$recentGames = $this->repo->findFinishedForUserSince($user, $since);
|
||||
|
||||
foreach ($recentGames as $game) {
|
||||
if (!$game->updated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$month = $game->updated->format('Y-m');
|
||||
if (!isset($monthlyData[$month])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isRed = $game->red?->id === $userId;
|
||||
$myPts = $isRed ? $game->redPoints : $game->bluePoints;
|
||||
$oppPts = $isRed ? $game->bluePoints : $game->redPoints;
|
||||
$resign = $game->resign;
|
||||
$myColor = $isRed ? 'red' : 'blue';
|
||||
$oppColor = $isRed ? 'blue' : 'red';
|
||||
|
||||
$result = 'draws';
|
||||
if ($resign === $myColor) {
|
||||
$result = 'losses';
|
||||
} elseif ($resign === $oppColor) {
|
||||
$result = 'wins';
|
||||
} elseif ($myPts !== null && $oppPts !== null) {
|
||||
if ($myPts > $oppPts) $result = 'wins';
|
||||
elseif ($myPts < $oppPts) $result = 'losses';
|
||||
}
|
||||
|
||||
$monthlyData[$month][$result]++;
|
||||
}
|
||||
|
||||
return $monthlyData;
|
||||
}
|
||||
|
||||
private function buildRecentGamesSeries(User $user, int $userId): array
|
||||
{
|
||||
$recent = $this->repo->findRecentFinishedForUser($user, 15);
|
||||
$recent = array_reverse($recent);
|
||||
|
||||
$labels = [];
|
||||
$mines = [];
|
||||
$bonus = [];
|
||||
|
||||
foreach ($recent as $i => $game) {
|
||||
$isRed = $game->red?->id === $userId;
|
||||
$labels[] = '#' . ($i + 1);
|
||||
$mines[] = (int)($isRed ? $game->redPoints : $game->bluePoints);
|
||||
$bonus[] = (float)($isRed ? $game->redBonusPoints : $game->blueBonusPoints) ?: 0;
|
||||
}
|
||||
|
||||
return ['labels' => $labels, 'mines' => $mines, 'bonus' => $bonus];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* Class ProfileGameDto
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class ProfileGameDto implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $id,
|
||||
public ?string $uuid,
|
||||
public string $redName,
|
||||
public string $blueName,
|
||||
public ?string $redAvatar,
|
||||
public ?string $blueAvatar,
|
||||
public ?int $redPoints,
|
||||
public ?int $bluePoints,
|
||||
public ?bool $redExplodedBomb,
|
||||
public ?bool $blueExplodedBomb,
|
||||
public ?string $resign,
|
||||
public ?string $created,
|
||||
public ?string $date,
|
||||
public bool $isRed,
|
||||
public string $result,
|
||||
public ?int $myPoints,
|
||||
public ?int $oppPoints,
|
||||
public float $redBonusPoints,
|
||||
public float $blueBonusPoints,
|
||||
public array $redBonusStats,
|
||||
public array $blueBonusStats,
|
||||
) {
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Entity\PlayedGame;
|
||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||
|
||||
/**
|
||||
* Class ProfileGameDtoFactory
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class ProfileGameDtoFactory
|
||||
{
|
||||
public function __construct(private CacheManager $cacheManager) { }
|
||||
|
||||
public function create(PlayedGame $game, int $userId): ProfileGameDto
|
||||
{
|
||||
$isRed = $game->red?->id === $userId;
|
||||
$resign = $game->resign;
|
||||
$myColor = $isRed ? 'red' : 'blue';
|
||||
$oppColor = $isRed ? 'blue' : 'red';
|
||||
$myPts = $isRed ? $game->redPoints : $game->bluePoints;
|
||||
$oppPts = $isRed ? $game->bluePoints : $game->redPoints;
|
||||
|
||||
$redAvatarPath = $game->red?->avatarPath;
|
||||
$blueAvatarPath = $game->blue?->avatarPath;
|
||||
|
||||
return new ProfileGameDto(
|
||||
id: $game->id,
|
||||
uuid: $game->uuid?->toRfc4122(),
|
||||
redName: $game->red?->getUsername() ?? $game->redAnon?->userName ?? 'Guest',
|
||||
blueName: $game->blue?->getUsername() ?? $game->blueAnon?->userName ?? 'Guest',
|
||||
redAvatar: $redAvatarPath ? $this->cacheManager->generateUrl($redAvatarPath, 'avatar_thumb') : null,
|
||||
blueAvatar: $blueAvatarPath ? $this->cacheManager->generateUrl($blueAvatarPath, 'avatar_thumb') : null,
|
||||
redPoints: $game->redPoints,
|
||||
bluePoints: $game->bluePoints,
|
||||
redExplodedBomb: $game->redExplodedBomb,
|
||||
blueExplodedBomb: $game->blueExplodedBomb,
|
||||
resign: $resign,
|
||||
created: $game->created?->format('Y-m-d H:i'),
|
||||
date: $game->updated?->format('Y-m-d H:i'),
|
||||
isRed: $isRed,
|
||||
result: $this->resolveResult($resign, $myColor, $oppColor, $myPts, $oppPts),
|
||||
myPoints: $myPts,
|
||||
oppPoints: $oppPts,
|
||||
redBonusPoints: $game->redBonusPoints ?? 0.0,
|
||||
blueBonusPoints: $game->blueBonusPoints ?? 0.0,
|
||||
redBonusStats: $game->redBonusStats ?? [],
|
||||
blueBonusStats: $game->blueBonusStats ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveResult(
|
||||
?string $resign,
|
||||
string $myColor,
|
||||
string $oppColor,
|
||||
?int $myPts,
|
||||
?int $oppPts,
|
||||
): string {
|
||||
if ($resign === $myColor) {
|
||||
return 'loss';
|
||||
}
|
||||
|
||||
if ($resign === $oppColor) {
|
||||
return 'win';
|
||||
}
|
||||
|
||||
if ($myPts !== null && $oppPts !== null) {
|
||||
if ($myPts > $oppPts) {
|
||||
return 'win';
|
||||
}
|
||||
|
||||
if ($myPts < $oppPts) {
|
||||
return 'loss';
|
||||
}
|
||||
}
|
||||
|
||||
return 'draw';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Entity\UserStats;
|
||||
|
||||
/**
|
||||
* Class ProfileStatsDto
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class ProfileStatsDto
|
||||
{
|
||||
public function __construct(
|
||||
public int $total,
|
||||
public int $wins,
|
||||
public int $losses,
|
||||
public int $draws,
|
||||
public int $minesHit,
|
||||
public int $winRate,
|
||||
public int $avgScore,
|
||||
public float $bonusPoints,
|
||||
public float $avgBonus,
|
||||
public int $bestChain,
|
||||
public int $blindHits,
|
||||
public int $edgeMines,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromUserStats(?UserStats $stats): self
|
||||
{
|
||||
if ($stats === null) {
|
||||
return self::empty();
|
||||
}
|
||||
|
||||
return new self(
|
||||
total: $stats->totalGames,
|
||||
wins: $stats->wins,
|
||||
losses: $stats->losses,
|
||||
draws: $stats->draws,
|
||||
minesHit: $stats->totalMines,
|
||||
winRate: $stats->getWinRate(),
|
||||
avgScore: $stats->getAvgScore(),
|
||||
bonusPoints: (float)$stats->totalBonusPoints,
|
||||
avgBonus: (float)$stats->avgBonus,
|
||||
bestChain: $stats->bestChain,
|
||||
blindHits: $stats->blindHits,
|
||||
edgeMines: $stats->edgeMines,
|
||||
);
|
||||
}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self(
|
||||
total: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0,
|
||||
minesHit: 0,
|
||||
winRate: 0,
|
||||
avgScore: 0,
|
||||
bonusPoints: 0.0,
|
||||
avgBonus: 0.0,
|
||||
bestChain: 0,
|
||||
blindHits: 0,
|
||||
edgeMines: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* Class ProfileViewDto
|
||||
*
|
||||
* @package App\Dto
|
||||
* @author Lang <https://www.splendidbear.org>
|
||||
* @category Class
|
||||
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||
* @link www.splendidbear.org
|
||||
* @since 2026. 04. 20.
|
||||
*/
|
||||
final readonly class ProfileViewDto
|
||||
{
|
||||
public function __construct(
|
||||
public ProfileStatsDto $stats,
|
||||
public array $recent,
|
||||
public array $gamesData,
|
||||
public ProfileChartDataDto $chartData,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toTemplateContext(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user