【サーバー】無料でクライアントサーバー型のゲームを制作する３

# はじめに
[前回は環境構築](https://qiita.com/kappysan/items/0fa9079468197780d712)をしました。
今回はサーバーサイドの実装を進めます。
[こちら](https://qiita.com/sano1202/items/6021856b70e4f8d3dc3d)のようなLaravel入門記事が参考になります。

# 仕様
以下の画像のようなランキング機能を実装します。
![ランキングモーダル.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664789/07b017bd-f0e3-ec8d-2954-d5ee8ccea07b.jpeg)
ランキングにはユーザー名とスコアを表示します。
ゲーム開始前にユーザー登録はなく、ランキング登録時のみユーザー登録できるようになっています。
ランキング登録時に、新規ユーザーの場合はスコア登録と同時にユーザー登録も行い、既存ユーザーの場合はハイスコアなら更新します。

# DB設計
## ユーザー情報テーブル
`user_profiles`
|Field|Type|Length|Allow Null|Kety|Default|Extra|
|---|---|---|---|---|---|---|
|id|INT|10|×|PRI||auto_increment|
|uuid|VARCHAR|255|×|UNI||None|
|user_name|VARCHAR|255|×|||None|
|created_at|TIMESTAMP||○||NULL|None|
|updated_at|TIMESTAMP||○||NULL|None|

## ユーザーランキングテーブル
`user_rankings`
|Field|Type|Length|Allow Null|Kety|Default|Extra|
|---|---|---|---|---|---|---|
|id|INT|10|×|PRI||auto_increment|
|user_profile_id|BIGINT|20|×|||None|
|score|INT|11|×|||None|
|created_at|TIMESTAMP||○||NULL|None|
|updated_at|TIMESTAMP||○||NULL|None|

# API設計
## ランキング登録API
`add_user_ranking`
* Params

|name|Type|memo|
|---|---|---|
|uuid|string|ユーザーを識別するID|
|user_name|string|ユーザー名|
|score|int|取得したスコア|

* Responses（新規ユーザーの場合）

|name|Type|memo|
|---|---|---|
|uuid|string|ユーザーを識別するID|

* Responses（既存ユーザーの場合）
なし

## ランキング情報取得API
`get_user_ranking`
* Params
なし

* Responses

|name|Type|memo|
|---|---|---|
|responseParams.user_name|string|ユーザー名|
|responseParams.score|int|ハイスコア|

## ユーザー情報取得API
`get_user_info`
* Params

|name|Type|memo|
|---|---|---|
|uuid|string|ユーザーを識別するID|

* Responses

|name|Type|memo|
|---|---|---|
|responseParams.user_name|string|ユーザー名|
|responseParams.high_score|int|ハイスコア|

# Laravelコマンド
Laravelではartisanコマンドで様々なファイルの作成を行います。
以降のコマンドの実行はLaravelをインストールしたディレクトリに移動し行ってください。
私は`Sample_Api/laravel`に入れたので、以下のコマンドでこのディレクトリに移動しておきます。
```
cd Sample_Api/laravel
```

# マイグレーション
テーブルの作成を行います。
まず以下のコマンドでuser_profilesテーブルのマイグレーションファイルを作成します。
```
php artisan make:migration create_user_profiles_table
```
`laravel/database/migrations`ディレクトリにマイグレーションファイルが作成されました。

DB設計に合わせて以下のように記述します。
```
    public function up()
    {
        Schema::create('user_profiles', function (Blueprint $table) {
          $table->increments('id');
          $table->string('uuid')->unique();
          $table->string('user_name');
          $table->timestamps();
        });
    }
```
次は同じようにuser_rankingsテーブルのマイグレーションファイルを作成します。
以下のコマンドを実行します。
```
php artisan make:migration create_user_rankings_table
```
以下のように記述します。
```
    public function up()
    {
        Schema::create('user_rankings', function (Blueprint $table) {
          $table->increments('id');
          $table->bigInteger('user_profile_id')->references('id')->on('user_profiles');
          $table->integer('score');
          $table->timestamps();
        });
    }
```
準備が完了しました。
マイグレーションを実行し、テーブルを作成します。
マイグレーション実行のコマンドは以下です。
```
php artisan migrate
```
Sequel proなどでDBを確認しましょう。
user_profilesテーブルとuser_rankingsテーブルが生成されていると思います。

# モデル
テーブルに対応するようモデルを作成します。
モデルはテーブルと自動でマッピングされるようになっています。
マッピングさせるため、必ずテーブル名の単数形を名付けるようにしてください。

まずはuser_profilesテーブルとマッピングするモデルを作成します。
以下のコマンドでモデルファイルを作成できます。
```
php artisan make:model UserProfile
```
次にuser_rankingsテーブルとマッピングするモデルを作成します。
以下のコマンドを実行します。
```
php artisan make:model UserRanking
```
モデルが作成できました。

# コントローラー
コントローラーにAPIの実装を書いていきます。
ユーザーランキングのAPIを作成するので、UserRankingControllerと名付けることにします。
コントローラーは以下のコマンドで作成できます。
```
php artisan make:controller UserRankingController
```

今回は３つのAPIを作成します。
①ランキング登録API
②ランキング情報取得API
③ユーザー情報取得API

①はadd_user_rankingメソッドに、②はget_user_rankingメソッドに、③はget_user_infoメソッドに記述することにします。
詳しい実装は後で書きます。

# ルーティング
リクエストを渡すメソッドを設定します。
APIのルーティングは`laravel/routes/app.php`に記述します。
Laravel 8.xでは以下のように書きます。
```
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserRankingController;

Route::group(['middleware' => ['api']], function(){
  Route::post('/user/ranking/add', [UserRankingController::class, 'add_user_ranking']);
  Route::get('/user/ranking/get', [UserRankingController::class, 'get_user_ranking']);
  Route::post('/user/info/get', [UserRankingController::class, 'get_user_info']);
});
```
１つ目が①ランキング登録API、２つ目が②ランキング情報取得API、３つ目が③ユーザー情報取得APIです。
コントローラーのパスをuseするのを忘れないようにしましょう。
また、`/user/ranking/add`のように頭に`/`を必ずつける必要があります。

POSTMANなどで接続の確認をしましょう。
例えば`http://localhost:8002/laravel/public/api/user/ranking/add`のリンクで1つ目のAPIに接続できます。

# ランキング情報取得APIの実装
ランキングを表示するときに、ランキング情報を取得するAPIを実装します。
リクエストはなし、レスポンスにはユーザー名とスコアを返します。

SQLで書くと以下のようになります。
```
SELECT up.user_name, ur.score
FROM user_profiles up
INNER JOIN user_rankings ur ON up.id = ur.user_profile_id
ORDER BY ur.score DESC, ur.updated_at ASC
```
順位付けの優先度は①スコアが高い②登録日が早いにしました。
これを[Laravelのクエリビルダ記法](https://readouble.com/laravel/8.x/ja/queries.html)で書くと以下のようになります。

```
$user_scores = DB::table('user_profiles')
      ->join('user_rankings', 'user_profiles.id', '=', 'user_rankings.user_profile_id')
      ->select('user_profiles.user_name', 'user_rankings.score')
      ->orderBy('user_rankings.score', 'desc')
      ->orderBy('user_rankings.updated_at', 'asc')
      ->get();
```
クエリビルダにはDBクラスを使います。
useするのを忘れないようにしましょう。
```
use Illuminate\Support\Facades\DB;
```
取得したデータをレスポンスで返します。
```
return ["responseParams" => $user_scores];
```
これで完成です。
```
use Illuminate\Support\Facades\DB;

class UserRankingController extends Controller
{
    public function get_user_ranking()
    {
      $user_scores = DB::table('user_profiles')
            ->join('user_rankings', 'user_profiles.id', '=', 'user_rankings.user_profile_id')
            ->select('user_profiles.user_name', 'user_rankings.score')
            ->orderBy('user_rankings.score', 'desc')
            ->orderBy('user_rankings.updated_at', 'asc')
            ->get();
      return ["responseParams" => $user_scores];
    }
}
```

# ランキング登録APIの実装
次はランキング登録するAPIを実装します。
新規ユーザーの場合は同時にユーザー登録も行い、既存ユーザーの場合はハイスコアのときのみスコアを更新します。
リクエストはuuid、ユーザー名、スコアで、新規ユーザーの場合は作成したuuidをレスポンスで返します。また、リクエストに応じてDBに変更を加えます。

まずはリクエストを受け取ります。
```
$uuid      = (string)$request->input('uuid');
$user_name = (string)$request->input('user_name');
$score     = (int)$request->input('score');
```
キャストはしておいた方が良いです。
キャストしなければintがstringになってしまいます。

次に、新規ユーザーか既存ユーザー判定し、それぞれ処理を書いています。
uuidがないなら新規、あるなら既存ユーザーですので、以下のように場合分けできます。
```
if($uuid == null) // 新規ユーザー
{
    ...
}
else // 既存ユーザー
{
    ...
}
```
まずは新規ユーザーの処理を書いていきます。
新規ユーザーなので、ユーザー登録をする必要があります。
uuidを生成してあげましょう。
[こちら](https://cufl.hateblo.jp/entry/2021/07/07/123218)を参考にしました。
Laravelがメソッドを用意してくれているので簡単です。
```
$new_uuid = (string)Str::uuid();
```
これだけです。
Strクラスをuseする必要があります。
```
use Illuminate\Support\Str;
```
これだけでも良いのですが、万が一生成されるuuidが被ったときのために、被ったら生成し直す処理を書きます。
```
// uuidの被り判定
$same_flg = true;

// 被りのないものができるまでuuid生成
while($same_flg == true)
{
  $new_uuid  = (string)Str::uuid();
  $same_uuid = UserProfile::where('uuid', '=', $new_uuid)
        ->first();
  if(empty($same_uuid))
  {
    $same_flg = false;
  }
}
```
これでuuidが生成できました。
生成したuuidをリクエストで得たuser_nameと一緒にユーザー登録します。
```
UserProfile::create([
    'uuid'      => $new_uuid,
    'user_name' => $user_name
]);
```
LaravelのEloquentを使っています。
これはすべてのモデルを継承しています。
使用するモデルをuseするようにしましょう。
```
use App\Models\UserProfile;
```
作成したuuidを最後にレスポンスで返します。
```
return $new_uuid;
```
次に作成したユーザーでランキング登録をします。
ユーザー情報テーブルとユーザーランキングテーブルはユーザー情報テーブルのidで紐づいています。
なので、生成したuuidからユーザー情報テーブルのidを取得し、そのidとリクエストで受け取ったスコアをユーザーランキングテーブルに登録するという手順になります。
まずは生成したuuidからユーザー情報テーブルのidを取得します。
```
$array_user_profile_id = UserProfile::where('uuid', '=', $new_uuid)
　　　->first(['id']);
$user_profile_id = $array_user_profile_id['id'];
```
idが配列となって抽出されるので、その後、値だけを抜き出すようにしています。
次にランキング登録をします。
```
UserRanking::create([
    'user_profile_id' => $user_profile_id,
    'score'           => $score
]);
```
使用するモデルのuseは忘れないようにしましょう。
```
use App\Models\UserRanking;
```
これで新規ユーザーの処理はできました。
次に既存ユーザーの処理です。
リクエストで得たuuidに紐づくユーザーランキングテーブルのuser_profile_idとscoreを取得し、そのscoreをリクエストで得たscoreが超えていれば、そのuser_profile_idのscoreを更新するという手順になります。
まずはリクエストで得たuuidに紐づくユーザーランキングテーブルのuser_profile_idとscoreを取得します。
クエリビルダで書きました。
```
$array_user_high_score = DB::table('user_rankings')
     ->join('user_profiles', 'user_rankings.user_profile_id', '=', 'user_profiles.id')
　　　->select('user_rankings.user_profile_id', 'user_rankings.score')
     ->where('user_profiles.uuid', '=', $uuid)
     ->first();
$user_profile_id = $array_user_high_score->user_profile_id;
$high_score      = $array_user_high_score->score;
```
これも先ほどと同じように配列で抽出されるため、その後、値だけを抜き出すようにしています。
値を抜き出す記法が先ほどと違いますが、クエリビルダの場合はこう書くようです。
参考記事は[こちら](https://prograshi.com/framework/laravel/php-error-stdclass/)です。
次に、取得したscoreがリクエストで得たscoreを超えていたとき、そのユーザーののscoreを更新する処理を書きます。
```
if($score > $high_score)
{
  UserRanking::where('user_profile_id', '=', $user_profile_id)
        ->update([
          'score' => $score
        ]);
}
```
以上で既存ユーザーの処理もできました。
これで完成です。
```
use Illuminate\Http\Request;
use App\Models\UserRanking;
use App\Models\UserProfile;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;

class UserRankingController extends Controller
{
    // ランキング登録
    public function add_user_ranking(Request $request)
    {
      // リクエストパラメータを受け取る
      $uuid      = (int)$request->input('uuid');
      $user_name = (string)$request->input('user_name');
      $score     = (int)$request->input('score');

      if($uuid == null) // 新規ユーザー
      {
        // uuidを生成する
        $new_uuid = self::_create_uuid();

        // ユーザー登録
        UserProfile::create([
          'uuid'      => $new_uuid,
          'user_name' => $user_name
        ]);

        // ランキング登録
        $array_user_profile_id = UserProfile::where('uuid', '=', $new_uuid)
              ->first(['id']);
        $user_profile_id = $array_user_profile_id['id'];
        UserRanking::create([
          'user_profile_id' => $user_profile_id,
          'score'           => $score
        ]);

        return $new_uuid;
      }
      else // 既存ユーザー
      {
        // uuidに紐づくユーザーのスコアを取得する
        $array_user_high_score = DB::table('user_rankings')
              ->join('user_profiles', 'user_rankings.user_profile_id', '=', 'user_profiles.id')
              ->select('user_rankings.user_profile_id', 'user_rankings.score')
              ->where('user_profiles.uuid', '=', $uuid)
              ->first();
        $user_profile_id = $array_user_high_score->user_profile_id;
        $high_score      = $array_user_high_score->score;

        // ハイスコアならランキングテーブルを更新する
        if($score > $high_score)
        {
          UserRanking::where('user_profile_id', '=', $user_profile_id)
                ->update([
                  'score' => $score
                ]);
        }
      }
    }

    // uuidを生成する
    private function _create_uuid()
    {
      // uuidの被り判定
      $same_flg = true;

      // 被りのないものができるまでuuid生成
      while($same_flg == true)
      {
        $new_uuid  = (string)Str::uuid();
        $same_uuid = UserProfile::where('uuid', '=', $new_uuid)
              ->first();
        if(empty($same_uuid))
        {
          $same_flg = false;
        }
      }
      return $new_uuid;
    }
}
```
uuidの生成は別のメソッドに分けました。
処理ごとにメソッドを分けておくと、使いまわせて便利です。

# ユーザー情報取得APIの実装
最後にユーザー情報を取得するAPIを実装します。
リクエストはuuid、で、ユーザー名とハイスコアをレスポンスで返します。

まずはリクエストを受け取ります。
```
$uuid = (string)$request->input('uuid');
```
次にこのuuidからユーザー情報テーブルのユーザー名と、ユーザーランキングテーブルのスコアを取得します。先ほどとほぼ同じです。
```
// uuidに紐づくユーザー情報を取得する
$array_user_data = DB::table('user_rankings')
      ->join('user_profiles', 'user_rankings.user_profile_id', '=', 'user_profiles.id')
      ->select('user_profiles.user_name', 'user_rankings.score')
      ->where('user_profiles.uuid', '=', $uuid)
      ->first();
$user_name  = $array_user_data->user_name;
$high_score = $array_user_data->score;
```
最後にレスポンスを返します。
```
return ["responseParams" => [[
          'user_name'  => $user_name,
          'high_score' => $high_score
        ]]];
```
これで完成です。
```
// ユーザー情報取得
public function get_user_info(Request $request)
{
  // リクエストパラメータを受け取る
  $uuid = (string)$request->input('uuid');

  // uuidに紐づくユーザー情報を取得する
  $array_user_data = DB::table('user_rankings')
        ->join('user_profiles', 'user_rankings.user_profile_id', '=', 'user_profiles.id')
        ->select('user_profiles.user_name', 'user_rankings.score')
        ->where('user_profiles.uuid', '=', $uuid)
        ->first();
  $user_name  = $array_user_data->user_name;
  $high_score = $array_user_data->score;

    return ["responseParams" => [[
      'user_name'  => $user_name,
      'high_score' => $high_score
    ]]];
}
```

# さいごに
今回はサーバーサイドをやりました。
[次回はクライアントサイド](https://qiita.com/kappysan/items/3f9fd102fb872cc98656)をやります。