mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 17:12:08 +00:00
Implement instant websocket room updates
This commit is contained in:
@@ -5,18 +5,22 @@ namespace App\Events;
|
||||
use App\Enums\RoomStateChange;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RoomStateChanged implements ShouldBroadcast
|
||||
class RoomStateChanged implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $state
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $roomId,
|
||||
public readonly int $version,
|
||||
public readonly RoomStateChange $change,
|
||||
public readonly ?array $state = null,
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): Channel
|
||||
@@ -30,13 +34,14 @@ class RoomStateChanged implements ShouldBroadcast
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{version: int, change: string}
|
||||
* @return array{version: int, change: string, state: array<string, mixed>|null}
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'version' => $this->version,
|
||||
'change' => $this->change->value,
|
||||
'state' => $this->state,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\RoundService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class RoundController extends Controller
|
||||
{
|
||||
@@ -14,28 +14,28 @@ class RoundController extends Controller
|
||||
private readonly RoundService $rounds,
|
||||
) {}
|
||||
|
||||
public function reset(Request $request, Room $room): RedirectResponse
|
||||
public function reset(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->rounds->reset($room);
|
||||
|
||||
return back();
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
public function reveal(Request $request, Room $room): RedirectResponse
|
||||
public function reveal(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->rounds->reveal($room);
|
||||
|
||||
return back();
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
public function nextRound(Request $request, Room $room): RedirectResponse
|
||||
public function nextRound(Request $request, Room $room): Response
|
||||
{
|
||||
$this->ensureModerator($request, $room);
|
||||
$this->rounds->startNext($room);
|
||||
|
||||
return back();
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
private function ensureModerator(Request $request, Room $room): void
|
||||
|
||||
@@ -6,7 +6,7 @@ use App\Http\Requests\SubmitVoteRequest;
|
||||
use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\VoteService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class VoteController extends Controller
|
||||
{
|
||||
@@ -17,7 +17,7 @@ class VoteController extends Controller
|
||||
public function store(
|
||||
SubmitVoteRequest $request,
|
||||
Room $room,
|
||||
): RedirectResponse {
|
||||
): Response {
|
||||
$player = $request->attributes->get('currentPlayer');
|
||||
|
||||
abort_unless(
|
||||
@@ -32,6 +32,6 @@ class VoteController extends Controller
|
||||
$request->validated('value'),
|
||||
);
|
||||
|
||||
return back();
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RoomLifecycleService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomRealtimeStateService $realtimeState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{player: Player, was_reopened: bool}
|
||||
*/
|
||||
@@ -49,6 +53,7 @@ class RoomLifecycleService
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::RoomReopened,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RoomParticipationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomRealtimeStateService $realtimeState,
|
||||
) {}
|
||||
|
||||
public function findByJoinCode(string $joinCode): ?Room
|
||||
{
|
||||
if ($joinCode === '') {
|
||||
@@ -92,6 +96,7 @@ class RoomParticipationService
|
||||
$room->id,
|
||||
$room->state_version,
|
||||
RoomStateChange::PlayerJoined,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
|
||||
return [$room, $player];
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Rooms;
|
||||
|
||||
use App\Models\Room;
|
||||
|
||||
class RoomRealtimeStateService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomStatisticsService $statistics,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* current_round: int,
|
||||
* revealed: bool,
|
||||
* state_version: int,
|
||||
* statistics: array{average: string|null, median: string|null}|null,
|
||||
* players: array<int, array{
|
||||
* id: int,
|
||||
* name: string,
|
||||
* is_moderator: bool,
|
||||
* is_owner: bool,
|
||||
* has_voted: bool,
|
||||
* vote: string|null
|
||||
* }>
|
||||
* }
|
||||
*/
|
||||
public function for(Room $room): array
|
||||
{
|
||||
$room->refresh();
|
||||
$room->load([
|
||||
'players' => fn ($query) => $query
|
||||
->active()
|
||||
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
|
||||
->orderByDesc('is_moderator')
|
||||
->orderBy('created_at'),
|
||||
'players.votes' => fn ($query) => $query
|
||||
->where('room_id', $room->id)
|
||||
->where('round', $room->current_round),
|
||||
]);
|
||||
|
||||
return [
|
||||
'current_round' => $room->current_round,
|
||||
'revealed' => $room->revealed_at !== null,
|
||||
'state_version' => $room->state_version,
|
||||
'statistics' => $this->statistics->currentRound($room),
|
||||
'players' => $room->players
|
||||
->map(function ($player) use ($room): array {
|
||||
$vote = $player->votes->first();
|
||||
|
||||
return [
|
||||
'id' => $player->id,
|
||||
'name' => $player->name,
|
||||
'is_moderator' => $player->is_moderator,
|
||||
'is_owner' => $player->user_id === $room->owner_id,
|
||||
'has_voted' => $vote !== null,
|
||||
'vote' => $room->revealed_at !== null ? $vote?->value : null,
|
||||
];
|
||||
})
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class RoundService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomRealtimeStateService $realtimeState,
|
||||
) {}
|
||||
|
||||
public function reset(Room $room): void
|
||||
{
|
||||
$stateVersion = DB::transaction(function () use ($room): int {
|
||||
@@ -38,6 +42,7 @@ class RoundService
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::VotesReset,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +74,7 @@ class RoundService
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::CardsRevealed,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,6 +107,7 @@ class RoundService
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::NextRound,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VoteService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomRealtimeStateService $realtimeState,
|
||||
) {}
|
||||
|
||||
public function submit(Room $room, Player $player, string $value): void
|
||||
{
|
||||
$stateVersion = DB::transaction(function () use ($room, $player, $value): int {
|
||||
@@ -52,6 +56,7 @@ class VoteService
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::VoteChanged,
|
||||
$this->realtimeState->for($room),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user