ゲームデータについて(2)固定データ

本記事(と次記事)ではゲームデータの一種である固定データについて、編集・コンバート・ロードする方法をUnity(C#)のサンプルコードを交えて簡単に説明します。

固定データの外部ファイル化

固定データを外部ファイル化し、必要な時(例えばタイトル画面からゲーム開始ボタンを押したタイミングなど)にロードさせるようにします。
固定データはExcelを使って編集し、バイナリにコンバートすることでゲームから読めるようにする方針とします。
これはゲームの開発環境でのみ行われる処理であり、ゲームの実行時には不要です。
UnityではAssets以下の任意の場所にEditorというディレクトリを作成すると、そこに配置されたものはゲームの実行時には参照されません。
参照:https://docs.unity3d.com/ja/2019.4/Manual/SpecialFolders.html
固定データを編集するExcelファイルやそのコンバートに必要なライブラリはAssets/Editor下に置くようにします。

NPOIのインストール

UnityのプロジェクトがExcelファイルを開けるようにするためにNPOIというライブラリを利用します。
Assets/EditorにNPOIというディレクトリを作成します。
https://www.nuget.org/packages/NPOI/
上記サイトから"Download Package"をクリック、
ダウンロードしたファイルの拡張子.nupkg.zipに変更して解凍、
解凍したファイルの/lib/net45/の中にある
NPOI.dll
NPOI.OOXML.dll
NPOI.OpenXml4Net.dll
NPOI.OpenXmlFormats.dll
をAssets/Editor/NPOIにコピーします。
また、以下のファイルも必要なので同じ要領でダウンロードして同くAssets/Editor/NPOIにコピーします。
https://www.nuget.org/packages/Portable.BouncyCastle/
からダウンロード・解凍したファイルの/lib/net40/の中にある
BouncyCastle.Crypto.dll
https://www.nuget.org/packages/SharpZipLib/
からダウンロード・解凍したファイルの/lib/net45/の中にある
ICSharpCode.SharpZipLib.dll

Excelデータの作成

Assets/EditorにGameDataというディレクトリを作成して固定データを編集するエクセルファイルを用意します。
ファイル名はfixedData.xlsxとして、下記のような感じでデータを作成します。
B列はコメントとして記述されている文字列でコンバート時には参照しません。
シート名は"Monster"とします。

コンバータの作成

Assets/Editor以下にFixedDataConverter.csというファイルを作成します。

using UnityEngine;
using UnityEngine.Assertions;
using UnityEditor;
using System.IO;
using NPOI.XSSF.UserModel;
using NPOI.SS.UserModel;

#if UNITY_EDITOR
public class FixedDataConverter : EditorWindow
{
    [MenuItem("Utility/Convert Fixed Data")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(FixedDataConverter));
    }
    public void OnGUI()
    {
        if (GUI.Button(new Rect(8, 8, 120, 32), "Monster Data"))
        {
            ConvertMonster();
        }
    }

    public void ConvertMonster()
    {
        using (FileStream readFileStream = new FileStream("Assets/Editor/GameData/fixedData.xlsx", FileMode.Open, FileAccess.Read))
        {
            XSSFWorkbook book = new XSSFWorkbook(readFileStream);
            if (book == null)
            {
                Assert.IsTrue(false);
                return;
            }

            ISheet sheet = book.GetSheet("Monster");
            if (sheet == null)
            {
                Assert.IsTrue(false);
                return;
            }
        }
    }
}
#endif // UNITY_EDITOR

UnityのMenuのUtilityから"Convert Fixed Data"が選択できることと、選択したらMonsterというボタンを含むダイアログが表示されることを確認します。
また、ボタンを押すことでConvertMonster()が実行され、Excelファイルが開かれてその中のMonsterシートにアクセスできることを確認します。
(コンバート処理は後ほど実装します。)

固定データを保持するクラスを作成

次にゲームデータのロード先として読み込んだデータを保持するクラスを作成します。
Assets以下にGameData/FixedDataというディレクトリを作成し、Assets/GameData/FixedDataにFixedMonsterData.csというファイルを作成します。
固定データはゲーム実行時は不変ですがコンバータ実行時には代入する処理が必要になるので、setアクセサをUNITY_EDITORで括る形で定義します。

using System;

[Serializable]
public class FixedMonsterData
{
    private int _hp = 0;
    public int hp
    {
        get { return _hp; }
#if UNITY_EDITOR
        set { _hp = value; }
#endif // UNITY_EDITOR
    }

    private int _mp = 0;
    public int mp
    {
        get { return _mp; }
#if UNITY_EDITOR
        set { _mp = value; }
#endif // UNITY_EDITOR
    }

    private int _attack = 0;
    public int attack
    {
        get { return _attack; }
#if UNITY_EDITOR
        set { _attack = value; }
#endif // UNITY_EDITOR
    }

    private int _defense = 0;
    public int defense
    {
        get { return _defense; }
#if UNITY_EDITOR
        set { _defense = value; }
#endif // UNITY_EDITOR
    }

    private int _agility = 0;
    public int agilty
    {
        get { return _agility; }
#if UNITY_EDITOR
        set { _agility = value; }
#endif // UNITY_EDITOR
    }

    public FixedMonsterData()
    {
    }

}
固定データ管理クラスの作成

固定データは種類が増えていくことを想定しているのでそれらを一元管理するクラスを作ります。
Assets/GameData/FixedDataにFixedDataManager.csを作成します。

using UnityEngine;
using UnityEngine.Assertions;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public static class FixedDataManager
{
    private static List<FixedMonsterData> _monsterList = null;
    public static List<FixedMonsterData> monsterList
    {
        get { return _monsterList; }
    }

    static FixedDataManager()
    {
        Initialize();
    }

    private static void Initialize()
    {
        _monsterList = new List<FixedMonsterData>();
    }

    private static T ReadSerializedData<T>(string path) where T : class
    {
        T data = null;
        TextAsset serializeFile = Resources.Load(path) as TextAsset;
        using (MemoryStream memoryStream = new MemoryStream(serializeFile.bytes))
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            data = binaryFormatter.Deserialize(memoryStream) as T;
        }
        return data;
    }

    public static void Serialize()
    {
        _monsterList = ReadSerializedData<List<FixedMonsterData>>("FixedData/FixedMonsterData");
    }

    public static int GetMonsterDataCount()
    {
        return _monsterList.Count;
    }
    public static bool CheckValidMonsterIndex(int index)
    {
        return (index >= 0) && (index < GetMonsterDataCount());
    }
    public static FixedMonsterData GetMonsterData(int index)
    {
        if (!CheckValidMonsterIndex(index))
        {
            Assert.IsTrue(false);
            return null;
        }
        return _monsterList[index];
    }
}

記事が長くなるので分割します。 次記事に続きます。

BinaryFormatterについて

上記のサンプルコードではBinaryFormatterというクラスを使用していますが、Microsoft .netによるとこのクラスは非推奨とされています。
参照:https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/binaryformatter-security-guide
要するに暗号化されないデータをサーバに送れるようになっていると攻撃に利用されるので危険、ということのようです。
サーバと通信しないスタンドアローンなゲームならもちろん気にする必要はありませんし、通信するゲームでもクライアント側のゲームデータをまるまるサーバに送るようなことをしない限り問題はないと思います。
ただ、ゲームデータが暗号化されていないとユーザーが自由に閲覧・編集できてしまうことには変わりないので、ユーザー間の公平性が重要になるオンライン仕様を実装するのであればゲームデータの暗号化は必須になります。

また、iOS環境ではBinaryFormatterが呼ばれる前に

Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes");

を呼ばないとエラーになるそうです。(iOS環境が手元に無いので試していません。)

ゲームデータについて(1)概要

ゲームが参照するデータの実装について

例えばあなたがRPGを作りたいとして、手始めに敵モンスターのデータを実装したいとします。
最もシンプルな実装方法として思い浮かぶのは、以下のようにソースに直接定義してしまう方法でしょうか。

public struct MonsterData
{
    private string name;
    private int hp;
    private int mp;
    private int attack;
    private int defense;

    public MonsterData( string name, int hp, int mp, int attack, int defense)
    {
        this.name = name;
        this.hp = hp;
        this.mp = mp;
        this.attack = attack;
        this.defense= defense;
    }
}

public class GameData
{
    MonsterData[] monsterDataArray =
    {
        new MonsterData( "モンスター1", 10, 10, 10, 10 ),
        new MonsterData( "モンスター2", 20, 20, 20, 20 ),
        new MonsterData( "モンスター3", 30, 30, 30, 30 ),
    };
}

確かにシンプルですが、このままではモンスターのステータスの数値を変更するたびにコンパイルする必要があります。
これらのデータを外部ファイルとしてソースの外に出してしまえば、調整する際はそのファイルを更新するだけで済むので、コンパイルで時間を無駄にすることを回避できます。
また、開発メンバーが複数いて、データの調整の担当者とデータ周りのソースコードの担当者が別であれば、両者が同一ファイルを編集することを避けることにもなります。
ゲームが参照する各種データをゲームデータと呼ぶことにして、ゲームデータの管理の手法についてこれからいくつかの記事に分けて簡単に説明したいと思います。

ゲームデータの区分

ゲームデータはその性質に応じて別々に扱うことが望ましいです。
可変か固定かという区分と数値かテキストかという区分について考えていきます。

可変か固定か

ゲームを通じて変化しないデータを固定データ、ゲームの進行によって変化するデータを可変データとします。
例えば味方キャラクターが成長してステータス値が変化する仕様があるのであれば、それらは可変データとして扱います。
一方で敵モンスターには成長する仕様がなければ固定データとして扱った方がよいでしょう。
また、味方キャラクターのデータでも成長前の初期ステータス値に関しては不変なので、これについては固定データとして扱った方が便利かも知れません。
その場合、可変データとしてのキャラクターデータ(現在ステータス)を初期化する際に固定データとしてのキャラクターデータ(初期ステータス)を参照するという形になります。
このように同じオブジェクトに関連するデータでも固定データと可変データとして別々に扱われることがあります。

数値か文字列か

キャラクターやモンスターの名前は当然文字列として扱うことになりますが、文字列データは数値データとは別に管理した方がよいです。
理由としては、まず文字列データには独自の特殊処理を実装する必要がある点が挙げられます。
システムメッセージなどに見られる

"[キャラクター名]は[アイテム名]を手に入れた。"

といった、ゲームの状況に応じて特定の文字列に差し替える処理がこれに当たります。
また、将来的に外国語版を作成するのであれば翻訳が必要な文字列データが一か所にまとまっていた方が何かと都合がいいでしょう。
これらの理由から文字列データを数値データとは別に管理していきます。

開発環境について

以降の記事では以下の環境を利用してゲームデータの実装方法について説明していく予定です。

Unity 2021.3.8
Visual Studio 2019

Webアプリ習作#14

デバッガの構築

XdebugVS Codeを利用してブレークポイントでの停止やステップ実行をできるようにする

Xdebugのインストール

phpinfoを出力する
プロジェクトディレクトリで以下コマンドを実行

php -i

もしくは適当な画面に以下を記述してその画面にアクセスするとphpinfoを出力できる

<?php
phpinfo();
?>

出力されたものをコピーして以下のページのフォームに書いて送信すると必要な.dllが提示される
https://xdebug.org/wizard
ダウンロードした.dllをC:\xampp\php\extにコピー
C:\xampp\php\php.iniに以下を追記

[XDebug]
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.mode = debug
xdebug.start_with_request = yes
zend_extension = コピーした.dllのファイル名

ローカルサーバを起動し直した上で再度phpinfoを出力してXdebugが有効になったことを確認

VSCodeのインストール

https://code.visualstudio.com/からダウンロード・インストール
インストールしたらVSCodeを起動

拡張機能 PHP Debugのインストール

拡張機能メニューの検索バーにPHP Debugと入力して表示されたものをインストール

VSCodeの設定

VSCodeの"フォルダを開く"からC:\xampp\htdocs\twiappを選択
選択した場所に.vscodeというディレクトリが生成される
VSCodeの"実行とデバッグ"からlaunch.jsonを作成(.vscode以下に生成される)
"環境の選択"でPHPを選択
launch.jsonを開いて、"name": "Listen for Xdebug"の項目のportをXdebugの設定と合わせる
XDebugの設定はphpinfoを出力すればxdebug.client_portという項目から確認できる(9003のはず)

デバッガの実行

VSCodeの"実行とデバッグ"から"Listen for Xdebug"を選択し再生ボタンを押す
適当な箇所にブレイクポイントを置く
ローカルサーバを起動してhttp://localhost:8000/にアクセスしてブレイクポイントで停止することを確認

Webアプリ習作#13

TwitterのアクセストークンをDBに保存

前記事の問題はTwitterのアクセストークンをDBに保存することで対応することに
social_userテーブルに格納する

social_usersテーブルの拡張
php artisan make:migration add_token_columns_to_social_users --table social_users

[日付]_add_token_columns_to_social_users.phpというファイルが生成されるので編集

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('social_users', function (Blueprint $table) {
            // ここからを追加.
            $table->string('token')->after('provider_user_id');
            $table->string('token_secret')->after('token');
            // ここまでを追加.
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('social_users', function (Blueprint $table) {
            // ここからを追加.
            $table->dropColumn('token_secret');
            $table->dropColumn('token');
            // ここまでを追加.
        });
    }
マイグレーションを実行
php artisan migrate

※エラー発生

   Illuminate\Database\QueryException  : SQLSTATE[23502]: Not null violation: 7 ERROR:  column "token" of relation "social_users" contains null values (SQL: alter table "social_users" add column "token" varchar(255) not null, add column "token_secret" varchar(255) not null)

NOT NULL制約付きのカラムのデータにnullが含まれてしまうというエラーが発生
既存のデータがある状態でNOT NULL 制約付きのカラムを追加しようとしているのが原因と思われるので既存のデータを消す

psqlを起動して既存データの削除
\c twiapp_db
delete from users;

usersとsocial_users,tweetsは連携しているのでusersのデータを消すとsocial_users,tweetsのデータも消える

マイグレーションを再度実行
php artisan migrate
psqlからsocial_usersを確認
\d social_users;

tokenとtoken_secretのカラムが追加されていることを確認

Twitterログインのcallbackを修正

app\Http\Controllers\TwitterAuthController.phpの以下を修正
追加されたsocial_usersのカラムにデータが格納されるように

    public function callback()
    {
        $providerUser = Socialite::driver('Twitter')->user();

        // 既に存在するユーザーかを確認
        $socialUser = SocialUser::where('provider_user_id', $providerUser->id)->first();

        if ($socialUser) {
            // 既存のユーザーはログインしてトップページへ
            Auth::login($socialUser->user, true);
            return redirect('/');
        }

        // 新しいユーザーを作成
        $user = new User();
        $user->unique_id = $providerUser->nickname;
        $user->name = $providerUser->name;
        $user->avatar = $providerUser->user['profile_image_url_https'];
        $user->bio = $providerUser->user['description'];

        $socialUser = new SocialUser();
        $socialUser->provider_user_id = $providerUser->id;
        // ここからを追加.
        $socialUser->token = $providerUser->token;
        $socialUser->token_secret = $providerUser->tokenSecret;
        // ここまでを追加.

        DB::transaction(function () use ($user, $socialUser) {
            $user->save();
            $user->socialUsers()->save($socialUser);
        });

        Auth::login($user, true);
        return redirect('/');
    }
ツイートを実行する処理を修正

app\Http\Controllers\TweetController.phpの以下を修正

    public function store()
    {
        //$user = Socialite::driver('Twitter')->user(); // ここを削除.
        // ここからを追加.
        $user = Auth::user();
        $socialUser = $user->socialUsers()->first();
        // ここまでを追加.
        $twitter = new TwitterOAuth( env('TWITTER_CLIENT_ID'),
            env('TWITTER_CLIENT_SECRET'),
            // ここからを修正.
            $socialUser->token,
            $socialUser->token_secret );
            // ここまでを修正.

        $body = 'テストツイートです';
    
        $twitter->post("statuses/update", [</br>
           "status" =>
                $body
            ]);
    
        $tweet = new Tweet();
        $tweet->body = $body;
        $tweet->user_id = Auth::user()->id;
        $tweet->save();

        return redirect('/');
    }
動作確認

http://localhost:8000/にアクセスしてログイン・テストツイート実行を一通り確認

仮画面の削除

resources/views/test.blade.php にはTwitterのアクセストークンが直書きされている
一通りの動作が確認出来たら不要なので削除する

Webアプリ習作#12

ツイート一覧画面からのログイン

index.blade.phpTwitterログインへのリンク(twitter.login)を追加する

ルーティング

routes/web.phpを修正

Route::prefix('auth')->group(function () {
    Route::get('twitter', 'TwitterAuthController@login')->name('twitter.login'); // ここを修正.
    Route::get('twitter/callback', 'TwitterAuthController@callback');
});
ルーティング確認
php artisan route:list
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
| Domain | Method   | URI                   | Name          | Action                                              | Middleware   |
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
|        | GET|HEAD | /                     |               | App\Http\Controllers\TweetController@index          | web          |
|        | GET|HEAD | api/user              |               | Closure                                             | api,auth:api |
|        | GET|HEAD | auth/twitter          | twitter.login | App\Http\Controllers\TwitterAuthController@login    | web          |
|        | GET|HEAD | auth/twitter/callback |               | App\Http\Controllers\TwitterAuthController@callback | web          |
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
ログインへのリンクを追加

resources/views/tweets/index.blade.phpを修正

<body>

// ここからを追加.
  <a href="{{ route('twitter.login') }}">Twitterでログイン</a>
// ここまでを追加.

  @foreach($tweets as $tweet) 
    <div>
      <img src="{{ $tweet->user->avatar }}" width="48" height="48">
      {{ $tweet->user->name }}
      {{ $tweet->created_at->format('Y/m/d H:i') }}
    </div>
    <div>
      {!! nl2br(e( $tweet->body )) !!}
    </div>
  @endforeach

</body>
確認

http://localhost:8000/にアクセス
一番上に"Twitterでログイン"のリンクが表示されるのでクリックしてログインができるかを確認
(確認前に一旦Twitterからログアウトしておく)

ツイート一覧画面からのテストツイート

resources/views/tweets/index.blade.phpにテストツイートボタンを作る

ルーティング

routes/web.phpを修正

Route::get('/', 'TweetController@index');
Route::post('tweets', 'TweetController@store')->name('tweets.store'); // ここを追加.

Route::prefix('auth')->group(function () {
    Route::get('twitter', 'TwitterAuthController@login')->name('twitter.login');
    Route::get('twitter/callback', 'TwitterAuthController@callback');
});
ルーティング確認
php artisan route:list
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
| Domain | Method   | URI                   | Name          | Action                                              | Middleware   |
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
|        | GET|HEAD | /                     |               | App\Http\Controllers\TweetController@index          | web          |
|        | GET|HEAD | api/user              |               | Closure                                             | api,auth:api |
|        | GET|HEAD | auth/twitter          | twitter.login | App\Http\Controllers\TwitterAuthController@login    | web          |
|        | GET|HEAD | auth/twitter/callback |               | App\Http\Controllers\TwitterAuthController@callback | web          |
|        | POST     | tweets                | tweets.store  | App\Http\Controllers\TweetController@store          | web          |
+--------+----------+-----------------------+---------------+-----------------------------------------------------+--------------+
テストツイートボタンの追加

resources/views/tweets/index.blade.phpを修正

<body>

  <a href="{{ route('twitter.login') }}">Twitterでログイン</a>

// ここからを追加.
  <form method="POST" action="{{ route('tweets.store') }}">
    @csrf
    <button type="submit">テストツイート</button>
  </form>
// ここまでを追加.

  @foreach($tweets as $tweet) 
    <div>
      <img src="{{ $tweet->user->avatar }}" width="48" height="48">
      {{ $tweet->user->name }}
      {{ $tweet->created_at->format('Y/m/d H:i') }}
    </div>
    <div>
      {!! nl2br(e( $tweet->body )) !!}
    </div>
  @endforeach

</body>
ツイートをDBに保存する処理の作成

TweetControllerにstoreメソッドを作成しそこでツイートすると同時にDBに保存する
app/Http/Controllers/TweetController.phpを修正

use App\Models\Tweet;
use Illuminate\Http\Request;
// ここからを追加.
use Auth;
use Socialite;
use Abraham\TwitterOAuth\TwitterOAuth;
// ここまでを追加.

class TweetController extends Controller
{
    public function index()
    {
        $tweets= Tweet::all()->sortByDesc('created_at');
        return view('tweets.index', ['tweets' => $tweets]);
    }

// ここからを追加.
    public function store()
    {
        $user = Socialite::driver('Twitter')->user();
        $twitter = new TwitterOAuth( env('TWITTER_CLIENT_ID'),
            env('TWITTER_CLIENT_SECRET'),
            $user->token,
            $user->tokenSecret );

        $body = 'テストツイートです';
    
        $twitter->post("statuses/update", [
           "status" =>
                $body
            ]);
    
        $tweet = new Tweet();
        $tweet->body = $body;
        $tweet->user_id = Auth::user()->id;
        $tweet->save();
    }
// ここまでを追加.
}

※エラー発生

$user = Socialite::driver('Twitter')->user();

上記実行時に下記エラーが発生する

Invalid request. Missing OAuth verifier.

Twitterのログインが成功した後ならSocialite::driver('Twitter')->user()を呼ぶたびにユーザー情報を取得できると思い込んでいたがどうやらそうではない模様
ログイン直後に得たユーザー情報をDBに格納するかセッションに覚えさせる必要があるとのこと
参考:https://stackoverflow.com/questions/42193340/laravel-socialite-invalid-request-missing-oauth-verifier-twitter

Webアプリ習作#11

ツイート一覧画面の作成

DBに格納されているツイートを一覧で表示できるようにする

ルーティングの追加

routes/web.phpを編集

// ここからを削除.
//Route::get('/', function () {
//    return view('test');
//});
// ここまでを削除.
Route::get('/', 'TweetController@index'); // これを追加.
ルーティングの確認
php artisan route:list
+--------+----------+-----------------------+------+-----------------------------------------------------+--------------+
| Domain | Method   | URI                   | Name | Action                                              | Middleware   |
+--------+----------+-----------------------+------+-----------------------------------------------------+--------------+
|        | GET|HEAD | /                     |      | App\Http\Controllers\TweetController@index          | web          |
|        | GET|HEAD | api/user              |      | Closure                                             | api,auth:api |
|        | GET|HEAD | auth/twitter          |      | App\Http\Controllers\TwitterAuthController@login    | web          |
|        | GET|HEAD | auth/twitter/callback |      | App\Http\Controllers\TwitterAuthController@callback | web          |
+--------+----------+-----------------------+------+-----------------------------------------------------+--------------+
コントローラーの作成

プロジェクトディレクトリにて以下を実行

php artisan make:controller TweetController

'app/Http/Controllers/TweetController.php' が作成されるので編集

namespace App\Http\Controllers;

// ここからを追加.
use App\Models\Tweet;
// ここまでを追加.
use Illuminate\Http\Request;

class TweetController extends Controller
{
    // ここからを追加.
    public function index()
    {
        $tweets= Tweet::all()->sortByDesc('created_at');
        return view('tweets.index', ['tweets' => $tweets]);
    }
    // ここまでを追加.
}
ツイート一覧画面の作成

resources/viewsにtweetsディレクトリを作成してindex.blade.phpというファイルを作成

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>
    ツイート一覧
  </title>
</head>

<body>

  @foreach($tweets as $tweet) 
    <div>
      <img src="{{ $tweet->user->avatar }}" width="48" height="48">
      {{ $tweet->user->name }}
      {{ $tweet->created_at->format('Y/m/d H:i') }}
    </div>
    <div>
      {!! nl2br(e( $tweet->body )) !!}
    </div>
  @endforeach

</body>

</html>
確認

http://localhost:8000/ にアクセスしてツイート一覧が表示されていることを確認

Webアプリ習作#10

Tweetをデータとして管理する

アプリを介したツイートをアプリ側でも見れるようにしたいのでツイートの情報をDBのデータとして管理する仕組みを作成

tweetsテーブルの作成

プロジェクトディレクトリにて以下を実行

php artisan make:migration create_tweets_table --create=tweets

database/migrations下に[日付]_create_tweets_table.phpというファイルが生成されるので下記のように修正

    public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->bigIncrements('id');
            // ここからを追加.
            $table->text('body');
            $table->bigInteger('user_id')->unsigned()->index();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            // ここまでを追加.
            $table->timestamps();
        });
    }
マイグレーションの実行

プロジェクトディレクトリにて以下を実行

php artisan migrate
DBの確認

psqlを起動してDBに接続

\c twiapp_db

テーブル一覧を確認してtweetsテーブルが追加されていることを確認

\dt

テーブルの中身を確認

\d tweets
     列     |             タイプ             | 照合順序 | Null 値を許容 |             デフォルト
------------+--------------------------------+----------+---------------+------------------------------------
 id         | bigint                         |          | not null      | nextval('tweets_id_seq'::regclass)
 body       | text                           |          | not null      |
 user_id    | bigint                         |          | not null      |
 created_at | timestamp(0) without time zone |          |               |
 updated_at | timestamp(0) without time zone |          |               |
Modelの作成

プロジェクトディレクトリにて以下を実行

php artisan make:model Models/Tweet

app/Models/Tweet.phpが作成されることを確認

Userモデルと連携

app/Models/User.phpを開いて以下を追加

    public function tweets()
    {
        return $this->hasMany(Tweet::class);
    }

app/Models/Tweet.phpに以下を追加

    public function user()
    {
        return $this->belongsTo(User::class);
    }
ツイート実行と同時にデータを追加するように修正

resources/views/test.blade.phpを修正

<?php
// ここからを追加.
namespace App\Models;
use Illuminate\Support\Facades\Auth;
// ここまでを追加.
use Abraham\TwitterOAuth\TwitterOAuth;
function testTweet(){
    $twitter = new TwitterOAuth( env('TWITTER_CLIENT_ID'),
        env('TWITTER_CLIENT_SECRET'),
        "メモしたtokenの文字列",
        "メモしたtokenSecretの文字列" );

    $body = 'テストツイートです'; // ここを追加.

    $twitter->post("statuses/update", [
       "status" =>
            $body // ここを修正.
        ]);

    // ここからを追加.
    $tweet = new Tweet();
    $tweet->body = $body;
    $tweet->user_id = Auth::user()->id;
    $tweet->save();
    // ここまでを追加.
}
動作確認

http://localhost:8000にアクセスして"テストツイートをする"をクリック
Twitter上にテストツイートが追加されていると同時にDBのtweetsテーブルにデータが追加されていることを確認

\c twiapp_db
select * from tweets;