where('join_code', $joinCode) ->first(); } public function findExistingPlayer( Room $room, ?User $user, mixed $sessionPlayerId, ): ?Player { if ($user !== null) { return $room->players() ->where('user_id', $user->id) ->first(); } if (! is_numeric($sessionPlayerId)) { return null; } return $room->players()->find((int) $sessionPlayerId); } /** * @param array{join_code: string, player_name: string} $data * @param array $sessionPlayerIds * @return array{Room, Player} */ public function join(array $data, ?User $user, array $sessionPlayerIds): array { [$room, $player] = DB::transaction(function () use ($data, $user, $sessionPlayerIds): array { $room = Room::query() ->where('join_code', $data['join_code']) ->lockForUpdate() ->firstOrFail(); if ($room->isClosed()) { throw ValidationException::withMessages([ 'join_code' => 'Dieser Raum ist geschlossen.', ]); } $player = $this->findExistingPlayer( $room, $user, $sessionPlayerIds[$room->id] ?? null, ); if ($player === null) { $player = $room->players()->create([ 'user_id' => $user?->id, 'name' => $data['player_name'], 'is_moderator' => false, 'last_seen_at' => now(), ]); } else { $player->update([ 'last_seen_at' => now(), 'inactive_at' => null, ]); } $room->update([ 'last_activity_at' => now(), 'state_version' => $room->state_version + 1, ]); return [$room, $player]; }); RoomStateChanged::dispatch( $room->id, $room->state_version, RoomStateChange::PlayerJoined, ); return [$room, $player]; } }