目次
はじめに
この記事では、#1(https://developers.10antz.co.jp/archives/1897) で作成したマスターデータをコード側でどう扱うのかをPHPを使用して解説します。
- マスターデータの格納先について
- PHPを使ってCSVからレコードを取得する方法
- クラスオブジェクトにマッピングする方法
マスターデータの格納先について
マスターデータの格納先には、以下のような持ち方があります。
RDBMS | NoSQL | ローカルファイル | |
形式 | MySQL、PostgreSQLなど | MongoDB、Redis、memcachedなど | CSV、TSV、Excelなど |
特徴 | ・フレームワーク/ライブラリ等のサポートが手厚い
・SQLで自由度の高いクエリを実行できる ・性能を上げる場合、一手間かける必要がある |
・高速なデータ取得が可能
・アプリケーション側で解決しなくてはならない問題が多い ・性能を上げやすい |
・実装が単純
・エンジニアでない人でも見ることができる ・性能を上げやすい |
ブラウザゲームの場合クライアントからもサーバー内にあるマスターデータを見れる必要があるので、マスターデータをサーバー側で管理します。
この記事では、ローカルファイルに格納した場合の具体例として、PHPでCSVをマスタデータとして使用するケースを説明します。
PHPを使ってCSVからレコードを取得する方法
PHPからCSVを取得する方法は、多くあると思いますがこの記事では「SplFileObject クラス」を利用します。
https://www.php.net/manual/ja/class.splfileobject.php
SplFileObject クラスは、ファイルをオブジェクト指向で扱う為のクラスで、SPL拡張モジュールのファイル操作クラスです。
※ SPL(Standard PHP Library)は、標準的な処理の為のインターフェイスやクラスを集めた拡張モジュールです。PHP 5.0.0以降はデフォルトで組み込まれています。
先にコードを貼っておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
class CsvManager extends \SplFileObject { /** @var array */ private static $_instances = array(); /** * インスタンス作成 * ・一度開いたCSVファイルのインスタンスは保存しておく(Singleton パターン) * @param $_fileName * @return $this */ public static function create($_fileName) { if(array_key_exists($_fileName, static::$_instances) === false) { static::$_instances[$_fileName] = new static($_fileName); } return static::$_instances[$_fileName]; } /** * CsvManager constructor. * @param string $_fileName */ public function __construct($_fileName) { parent::__construct(static::getFilePath($_fileName), 'r', false); $this->setFlags(parent::READ_CSV); } /** * 対象CSVまでのファイルパスを取得 * @param string $_filename * @return string */ private static function getFilePath($_filename = '') { return sprintf('%s/../resources/csv/'. $_filename .'.csv', __DIR__); } /** * CSVを配列として取得する * @param string $_primaryKey * @return array */ public function toArray($_primaryKey = 'id') { $arrayedCsv = array(); foreach($this as $key => $row) { if($key === 0) { // 1行目はヘッダー行として取り込み $headers = $row; } else { // 正しくない形式のレコードは除外する if(count($headers) != count($row) || $row[0] === null) { continue; } // ヘッダーを配列のキーとして新しい配列を作成する $combinedArray = array_combine($headers, $row); if (is_array($combinedArray)) { // プライマリーキーを配列のキーとして配列を追加 $arrayedCsv[$combinedArray[$_primaryKey]] = $combinedArray; } } } return $arrayedCsv; } } |
1. CSV取得を管理するクラスを作成して、SplFileObject クラスを継承させます。
2. SplFileObject クラスはコンストラクタに以下の引数を渡すことで、該当するCSVファイルのレコードをオブジェクトにしてくれます。
filename(読み込むファイル。)
open_mode(ファイルをオープンするときのモード。r,w など)
use_include_path(filename を探すのに include_path を探索するかどうか。)
context(stream_context_create() で作られる有効なコンテキストリソース。)
詳しくはこちらを参考にしてください。https://www.php.net/manual/ja/splfileobject.construct.php
3. 次に、必要に応じて以下のフラグをセットしてください。
SplFileObject::DROP_NEW_LINE (行末の改行を読み飛ばします。)
SplFileObject::READ_AHEAD (先読み/巻き戻しで読み出します。)
SplFileObject::SKIP_EMPTY (ファイルの空行を読み飛ばします。期待通りに動作させるには、READ_AHEAD フラグを有効にしないといけません。)
SplFileObject::READ_CSV (CSV 列として行を読み込みます。)
今回は、READ_CSVのみセットしました。
4. ヘッダー(カラム名)とレコードを紐付ける必要がある為、toArrayという自作のメソッドを実装し、オブジェクトを整形できるようにしています。
5. このクラスを呼ぶ度に、SplFileObjectのコンストラクタでCSVファイルを開いてしまい無駄な処理が発生する可能性があるので、一度作成したインスタンスは2度以上作成しないようにする処理を追加します。
create() という関数を作成、同じCSVファイルのインスタンスが存在するかどうかを確認し、存在しなければ新たに作成します。
外からこのクラスのインスタンスを作成する際は、create() 関数を使用することで同じインスタンスを使い回すことができるようになります。
こちらのクラスを実行するとこのような配列が出力されていると思います。
1 2 3 4 5 |
$csv = App\Models\CsvManager::create("dungeon/area")->toArray('area'); foreach($csv as $key => $value) { var_dump($key, $value); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
int(1) array(4) { ["area"]=> string(1) "1" ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> string(1) "1" ["areaReleasedAt"]=> string(19) "2021/01/01 00:00:00" } int(2) array(4) { ["area"]=> string(1) "2" ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> string(1) "1" ["areaReleasedAt"]=> string(19) "2021/02/01 00:00:00" } int(3) array(4) { ["area"]=> string(1) "3" ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> string(1) "1" ["areaReleasedAt"]=> string(19) "2021/03/01 00:00:00" } |
CSVのプライマリーキーが配列のキー、カラム名が連想配列のキーになっているので array[1][ ‘areaReleasedAt’ ] のようにするだけで、エリア1の開放時間を取得できます。
クラスオブジェクトにマッピングする方法
業務で扱っていく上で、array[1][ ‘areaReleasedAt’ ] ではなく area[1]->areaReleasedAt のようにクラスからプロパティにアクセスして値を取得したいケースが出てくると思います。
ここでは、「PHPを使ってCSVからレコードを取得する方法」で取得した配列をクラスオブジェクトにマッピングする方法を解説します。
先にコードを貼っておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
class Master { /** @var string ファイル名(拡張子除く) */ static $filename = ''; /** @var string 対象CSVファイルのプライマリーキー */ static $primaryKey = 'id'; public function __construct(Array $properties = array()) { // 配列の中身をクラスのプロパティにマッピングする foreach($properties as $key => $value) { // 数値形式ならint型にキャストする $this->{$key} = is_numeric($value) ? (int)$value : $value; } } /** * 全件検索 * @return array */ public static function findAll() { $lines = self::getCSV(); return self::toObject($lines); } /** * プライマリーキーの値で検索 * @param null $id * @return mixed */ public static function findById($id = null) { $lines = self::getCSV(); return self::toObject($lines)[$id]; } /** * 対象のCSVファイルからデータを取得 * @return array */ private static function getCsv() { return CsvManager::create(static::$filename)->toArray(static::$primaryKey); } /** * 取得したCSVデータの配列をクラスオブジェクトにマッピングする * @param $lines * @return array */ protected static function toObject($lines) { $objects = array(); foreach ($lines as $line) { // 継承先のプロパティにCSVレコードを設定する $instance = new static($line); // 配列のキーをCSVレコードの主キーにする $objects[$instance->{static::$primaryKey}] = $instance; } return $objects; } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
class Area extends Master { /** @var string ファイル名 */ static $filename = 'dungeon/area'; /** @var string 主キー */ static $primaryKey = 'area'; public $area; public $areaClearReward; public $areaClearRewardNum; public $areaReleasedAt; } |
1. マッピング先のクラスでは各CSVファイル毎に共通の処理が発生するので、それらの処理を持つMasterクラスを作成します。
2. Masterクラスの継承先クラスを作成します。
以下の設定を行なってください。
対象CSVのファイル名(static 変数)
対象CSVの主キーカラム名(static 変数)
対象CSVファイルのカラム名と一致するプロパティ(public 変数)
3. 外部からCSVレコードを検索するトリガーが必要になるので、Masterクラスへ findAll(全件検索) と findById(主キー検索)メソッドを実装します。
このメソッドの中では、以下のことをします。
・CSVレコードの取得 → $lines = self::getCSV();
・取得したCSVレコードを継承先のクラスプロパティにマッピング → self::toObject($lines)
toObject()メソッドでは、CSVファイルのカラム名と継承先クラスのプロパティ名が一致していたら値をプロパティにセットする処理をしています。
継承先クラスへのマッピング方法は賛否両論あると思いますが、このやり方が一番簡単だと思います。
上記のコードを実行してみます。
1 2 3 4 5 6 |
$masters = App\Models\Masters\Area::findAll(); foreach ($masters as $key => $master) { var_dump($key, $master); } var_dump($masters[1]->areaReleasedAt); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
int(1) object(App\Models\Masters\Area)#2 (4) { ["area"]=> int(1) ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> int(1) ["areaReleasedAt"]=> string(19) "2021/01/01 00:00:00" } int(2) object(App\Models\Masters\Area)#4 (4) { ["area"]=> int(2) ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> int(1) ["areaReleasedAt"]=> string(19) "2021/02/01 00:00:00" } int(3) object(App\Models\Masters\Area)#5 (4) { ["area"]=> int(3) ["areaClearReward"]=> string(21) "ガチャチケット" ["areaClearRewardNum"]=> int(1) ["areaReleasedAt"]=> string(19) "2021/03/01 00:00:00" } string(19) "2021/01/01 00:00:00" // $masters[1]->areaReleasedAt の結果 |
このように、クラスオブジェクトへマッピングされていることが確認できたと思います。
クラスオブジェクトとして扱えるようになると、コードの可読性と柔軟性がかなり上がるので是非このように実装してみてください。
この記事で扱った実装環境は以下になります。
PHP 7.3
フォルダ構成
├── README.md
├── app
│ ├── models
│ │ ├── CsvManager.php CSV管理用クラス
│ │ ├── Master.php マスターデータ管理用クラス
│ │ └── masters マッピングするクラスオブジェクト用フォルダ
│ └── resources
│ └── csv マスターデータ用フォルダ
│ ├── dungeon
│ │ ├── area.csv
│ │ └── floor.csv
├── composer.json
├── index.php
└── vendor
composer.json
1 2 3 4 5 6 7 |
{ "autoload": { "psr-4": { "App\\": "app/" } } } |
実装したものは、githubにあげています。
https://github.com/takahiko-tanaka-10antz/master-article
まとめ
CSVからレコードを取得して、クラスオブジェクトにマッピングするまでの流れを解説している記事があまりなかったので書いてみました。
今回標準のパッケージのみで実装しましたが、この記事での内容を簡単に行えるようなライブラリが恐らくあると思うので、そちらも一緒に調べると良いかもしれません。