プログラミング

トークン認証とサニタイズ編 PHPポケモン 38 コード配布あり

PHP PHPポケモン ポケモン
トークン認証とサニタイズ編 PHPポケモン 38 コード配布あり

構成の見直し

PHPポケモンも38回となり、大分作り込みが出来てきました。ここ最近はコードの説明ばかりでデモページなども準備出来ていませんでしたが、それには內部側の問題点が多かったためです。今回はその辺りをキレイに解決できるよう、本格的な構成の見直しをします。

 

ちなみにですが、どれぐらいの見直しをしたかというと

ほぼ全入れ替えというレベルです

 

ではまず、簡単にファイル構成を説明しておきます。

App(処理関係)
―Controllers
―Services
―Traits
 
Classes(クラス関係)
―Move
―Pokemon
―StateChange
―StatusAilment
―Type
 
Public(公開ディレクトリ)
―Assets
―index.php
 
Resources(テンプレート等)
―Lang
―Pages
―Partials
 
Storages(保存データ)
―Sessions

 

こんな感じです。

ClassesAppの中に入れたほうが良いか迷ったのですが、一旦現状のままにしています。ただ、ファイル数も多くなり分けて置くほうが可視性も管理も良くなるのであれば、状況に応じて変更します。

 

また、流石にオートローダーを使っている関係上公開ディレクトリに置くわけにはいかなくなったので、こちらも変更しました。もし後ほど配布するコードで遊ぶ人は、公開ディレクトリにPublicを指定するようにしてください。

 

トークン認証とは

PHPポケモンの最大の問題点は「Webブラウザで動く」ということです。なぜこれが問題点になっているかと言うと、本来ゲームでは「戻る(ヒストリーバック)」や「更新(ページリロード)」という機能は備えられていません。しかし、Webブラウザの機能としてそれらが存在しているため、これらは避けて通ることができません。

37回まで作り込んできたPHPポケモンでは、更新すると再度POSTが発生してしまうということがありました。 

経験値取得 → 更新 → 再度経験値が取得できる

バトル画面 → 戻る → エラー

 

更新に関しては、デバック時には役立つことが多かったのですが、これをプレイを想定すれば大きな欠陥であることは否めません。そこで今回採用したのが「トークン認証」です。

 

ページ最上部
/**
* トークン発行
*/
$_SESSION['__token'] = bin2hex(openssl_random_pseudo_bytes(32));

 

フォーム内
<input type="hidden" name="__token" value="<?=$_SESSION['__token']?>">

 

ページ生成時にトークンを発行して、セッションに格納しておきます。後は、各フォームにhiddenで同じトークンを送信するようにしておけば、次のページへ移管した際に照合して、トークンが異なっていれば処理を中断または行わないという選択肢が生まれます。

今回はこのトークン認証を使い、一致すればそのまま処理実行一致しなければポストデータを破棄するということで更新時の2重処理を回避しました。

 

サニタイズとは

次にサニタイズです。前回もこちらは簡易ながら実装済みでしたが、今回はもう少しだけしっかりとサニタイズ処理をかけていきます。

 

サニタイズ(Sanitize.php
<?php
 
// サニタイズ
class Sanitize
{
 
    /**
    * @var array
    */
    private $post = [];
 
    /**
    * @return void
    */
    public function __construct()
    {
        $this->post = $this->sanitize($_POST);
    }
 
    /**
    * @return array
    */
    public function sanitize($array)
    {
        $post = [];
        foreach($array ?? [] as $key => $data){
            if(preg_match('/^__/', $key)){
                // 接頭語にアンダーバーが2つついていればサニタイズ不要
                continue;
            }
            if(is_array($data)){
                // 配列ならループ
                $post[htmlspecialchars($key)] = $this->sanitize($data);
            }else{
                $post[htmlspecialchars($key)] = htmlspecialchars($data);
            }
 
        }
        return $post;
    }
 
    /**
    * @return array
    */
    public function getPost()
    {
        return $this->post;
    }
 
}

 

クラスとして用意しました。サニタイズクラスを呼び出すことで、ポストデータをサニタイズしたものをpostプロパティに格納してくれます。システム内では特別なものを除いてグローバル変数のPOSTデータを使用せず、サニタイズ後のものをgetPostで受け取ってから活用します。

 

処理の流れ

では、処理の流れも不具合に合わせて修正していきましょう。今までは各ページに合わせたコントローラーを呼び出し、そこから分岐をさせて処理をさせるというコントローラーを主軸にした構成でしたが、今回の見直しで以下の3ステップによる処理の流れを組み上げていきます。 

  1. ルーティング
  2. コントローラー
  3. サービス

 

それでは、1つずつ役割を見ていきましょう。

 

ルーティング

URLの存在がある以上、直接アクセスされてしまうと対処が難しくなります。なので、同一URLを用いたセッションによるページ判別を行い、それに合わせたテンプレートを返却するという方法に変更します。

 

ルーティング(Route.php
<?php
// ルーティング
class Route
{
    /**
    * @var string
    */
    private $name;
 
    /**
    *
    * @return void
    */
    public function __construct($name, $token)
    {
        $this->name = $name;
        if(
            !isset($_POST['__token']) ||
            ($_POST['__token'] !== $token)
        ){
            $_POST = [];
        }
    }
 
    /**
    * テンプレートパス取得
    * @return string
    */
    public function template():string
    {
        switch ($this->name) {
            // ホーム画面
            case 'home':
            $path = '/Resources/Pages/Home.php';
            break;
            // バトル画面
            case 'battle':
            $path = '/Resources/Pages/Battle.php';
            break;
            // デフォルト(初期設定)
            default:
            $path = '/Resources/Pages/Initial.php';
            break;
        }
        // テンプレートパスを返却
        return $path;
    }
 
}

 

先程作成したトークン認証はルーティングで行なっています。ログイン機能などがある場合はコントローラーでする方が分岐を作りやすいですが、PHPポケモンでは画面の手順を判別するだけで用いられているのでこれで問題ありません。

 

コントローラー

次にコントローラーの役割ですが、今までは各トレイトを読み込みながら作り込んでいましたが、できるだけ個別の処理は次のサービスにひとまとめにするようにして、根幹となる処理分岐のみを行うようにしました。

 

バトルコントローラー(BattleController.php
<?php
$root_path = __DIR__.'/../../..';
require_once($root_path.'/App/Controllers/Controller.php');
// サービス
require_once($root_path.'/App/Services/Battle/StartService.php');
require_once($root_path.'/App/Services/Battle/RunService.php');
require_once($root_path.'/App/Services/Battle/FightService.php');
// トレイト
require_once($root_path.'/App/Traits/Controller/BattleControllerTrait.php');
 
// バトル用コントローラー
class BattleController extends Controller
{
 
    use BattleControllerTrait;
 
    /**
    * 敵ポケモン格納用
    * @var object
    */
    protected $enemy;
 
    /**
    * 逃走を試みた回数
    * @var integer
    */
    public $run = 0;
 
    /**
    * ひんし状態の格納
    * @var array
    */
    private $fainting = [
        'friend' => false,
        'enemy' => false,
    ];
 
    /**
    * @return void
    */
    public function __construct()
    {
        // 親コンストラクタの呼び出し
        parent::__construct();
        // 引き継ぎ
        $this->takeOver();
        // 分岐処理
        $this->branch();
        // 次のターンへの分岐
        $this->nextTurn();
        // 親デストラクタの呼び出し
        parent::__destruct();
    }
 
    /**
    * 引き継ぎ処理
    * @return void
    */
    private function takeOver()
    {
        // にげるの実行回数を引き継ぎ
        if(isset($_SESSION['run'])){
            $this->run = $_SESSION['run'];
        }
        // ポケモンの引き継ぎ
        $this->takeOverPokemon($_SESSION['__data']['pokemon']);
        // 敵ポケモンの引き継ぎ
        $this->takeOverEnemy($_SESSION['__data']['enemy'] ?? '');
    }
 
    /**
    * アクションに合わせた分岐
    * @return void
    */
    private function branch()
    {
        try {
            // アクション分岐
            switch ($this->request('action')) {
                /******************************************
                * 開始
                */
                case 'battle':
                // サービス実行
                $service = new StartService;
                $service->excute();
                // 実行結果
                $this->enemy = $service->getResponse('enemy');
                $this->setMessage($service->getMessages());
                break;
                /******************************************
                * たたかう
                */
                case 'fight':
                // サービス実行
                $service = new FightService(
                    $this->pokemon,
                    $this->enemy,
                    $this->request('param'),
                );
                $service->excute();
                // 実行結果
                $this->fainting = $service->getResponse('fainting');
                $this->setMessage($service->getMessages());
                break;
                /******************************************
                * にげる
                */
                case 'run':
                // 回数をプラス
                $this->run++;
                // サービス実行
                $service = new RunService(
                    $this->pokemon,
                    $this->enemy,
                    $this->run
                );
                $service->excute();
                // 実行結果
                if($service->getResponse('result')){
                    // 成功
                    $this->setMessage($service->getMessages());
                    $_SESSION['__route'] = 'home';
                    // 破棄
                    unset(
                        $_SESSION['__data']['enemy'],
                        $_SESSION['__data']['rank'],
                        $_SESSION['__data']['sc'],
                        $_SESSION['__data']['run']
                    );
                    header("Location: ./", true, 307);
                    exit;
                }else{
                    // 失敗
                    $this->fainting = $service->getResponse('fainting');
                    $this->setMessage($service->getMessages());
                }
                break;
                /******************************************
                * バトル終了
                */
                case 'end':
                $_SESSION['__route'] = 'home';
                header("Location: ./", true, 307);
                exit;
                break;
                /******************************************
                * アクション未選択 or 実装されていないアクション
                */
                default:
                break;
            }
        } catch (\Exception $e) {
            // 初期画面へ移管
            $_SESSION['__route'] = 'initial';
            header("Location: ./", true, 307);
            exit;
        }
    }
 
    /**
    * 次のターンへの判定処理
    *
    * @return void
    */
    private function nextTurn()
    {
        // ひんしポケモンがでた場合の処理
        if($this->fainting['enemy'] || $this->fainting['friend']){
            $this->judgment();
            return;
        }
        // チャージ中なら再度アクション実行
        if($this->chargeNow()){
            $this->branch();
        }else{
            $this->setMessage('行動を選択してください');
        }
    }
 
}

 

コントローラーで行う内容は、あくまでアクションに対する分岐を担ってもらいます。分けることでそれぞれの処理で共通する内容をわけなければいけないということも出てきますが、処理を辿っていくという点においてはかなり改善しました。

 

サービス

最後に、新しく導入したサービスについてです。コントローラーの分岐を見てわかるように、それぞれの分岐に合わせてサービスクラスを呼び出しています。ダメージ計算や判定処理などのメイン処理はここで行なっています。

 

たたかう用サービス(FightService.php
<?php
$root_path = __DIR__.'/../../..';
// 親クラス
require_once($root_path.'/App/Services/Service.php');
// トレイト
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleAttackTrait.php');
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleCheckTrait.php');
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleEnemyAiTrait.php');
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleOrderGenelatorTrait.php');
 
/**
 * バトル開始
 */
class FightService extends Service
{
 
    use ServiceBattleAttackTrait;
    use ServiceBattleCheckTrait;
    use ServiceBattleEnemyAiTrait;
    use ServiceBattleOrderGenelatorTrait;
 
    /**
    * @var object Pokemon
    */
    protected $pokemon;
 
    /**
    * @var object Pokemon
    */
    protected $enemy;
 
    /**
    * @var integer
    */
    protected $move_number;
 
    /**
    * ひんし状態の格納
    * @var array
    */
    protected $fainting = [
        'friend' => false,
        'enemy' => false,
    ];
 
    /**
    * @return void
    */
    public function __construct($pokemon, $enemy, $move_number)
    {
        $this->pokemon = $pokemon;
        $this->enemy = $enemy;
        $this->move_number = $move_number;
    }
 
    /**
    * @return void
    */
    public function excute()
    {
        // 技取得
        $p_move = $this->selectMove();
        $e_move = $this->selectEnemyMove();
        // 行動順の取得
        $orders = $this->orderMove(
            [$this->pokemon, $this->enemy, $p_move],
            [$this->enemy, $this->pokemon, $e_move],
        );
        // 攻撃処理
        if(!$this->actionAttack($orders)){
            // どちらかがひんし状態になった
            $this->exportProperty('fainting');
            return;
        }
        // 行動後の状態異常・変化をチェック
        $this->afterCheck();
        // 指定したプロパティを返却
        $this->exportProperty('fainting');
    }
 
    /**
    * 選択された技を取得
    *
    * @return object Move
    */
    private function selectMove()
    {
        // 自ポケモンの技をインスタンス化
        if($this->move_number === ''){
            // 技が未選択の場合は「わるあがき」をセット
            return new Struggle;
        }else{
            return $this->pokemon
            ->getMove($this->move_number);
        }
    }
 
    /**
    * 相手ポケモンの技選択
    *
    * @return object Move
    */
    private function selectEnemyMove()
    {
        // AIで技選択
        $ai = $this->aiSelectMove();
        // 敵の技をインスタンス化
        return new $ai['class']($ai['remaining'], $ai['correction']);
    }
 
    /**
    * 行動順に攻撃処理
    *
    * @return boolean (false: ひんしポケモン有り)
    */
    private function actionAttack($orders)
    {
        foreach($orders as list($atk, $def, $move)){
            // 攻撃
            $this->attack($atk, $def, $move);
            // ひんしチェック
            $this->fainting = [
                $atk->getPosition() => $this->checkFainting($atk),
                $def->getPosition() => $this->checkFainting($def),
            ];
            // どちらかがひんし状態なら処理終了
            if($this->fainting['friend'] || $this->fainting['enemy']){
                $result = false;
                break;
            }
        } # endforeach
        // 結果返却
        return $result ?? true;
    }
 
    /**
    * 行動後のチェック処理
    *
    * @return void
    */
    private function afterCheck()
    {
        // 素早さで行動順を算出
        $order = $this->orderSpeed(
            [$this->pokemon, $this->enemy],
            [$this->enemy, $this->pokemon],
        );
        // 順番に処理
        foreach($order as list($atk, $def)){
            // ひんしチェック(開始時に行動側がひんし状態になっていないか確認)
            if($this->fainting[$atk->getPosition()]){
                // ひんし状態になった
                continue;
            }
            // 状態異常チェック
            $this->checkAfterSa($atk);
            // ひんし状況の格納
            $this->fainting[$atk->getPosition()] = $this->checkFainting($atk);
            // ひんしチェック
            if($this->fainting[$atk->getPosition()]){
                // どちらかがひんし状態になった
                continue;
            }
            // 状態変化チェック
            $this->checkAfterSc($atk, $def);
            // ひんし状況の格納
            $this->fainting = [
                $atk->getPosition() => $this->checkFainting($atk),
                $def->getPosition() => $this->checkFainting($def),
            ];
        } # endforeach
    }
}

 

個別のメソッドなどは、ほぼそのまま採用しています。あくまで記述箇所をバラしているに過ぎません。

こうしておけば、すべての処理でほぼ同じ順序とすることができるので、エラーが出た際にも対応がしやすくなります。

 

データの配布

第38回時点のコードGitHubにアップしているので、ぜひ参考にしてください。というよりも、今回の変更点については1割も説明できていないので、PHPポケモンに興味がある人や、これを学習の一環として取り組んでいる人は、コードを見てどういった構成になっているのかを読んで確かめてみてください。

 

デモページは近日公開予定です。プレイを楽しみにしている人は、今しばらくお待ちください。

 

まとめ

いかがだったでしょうか。

今回のPHPポケモンではシステムの根幹となる構成と、トークン認証やサニタイズといった制御部分について取り上げました。

学習目的であれば構成を考えて作り上げていくことは理解を深めるためにも良いですが、お客さんへ納品するものを作る場合はCMSやフレームワークを使って管理することをオススメします。特にシステム要素が強いものや規模が大きいものであれば尚更です。

プログラミング学習中の方は、是非参考にしてくださいね。

 

注目の記事

システムエンジニアとプログラマーの違いって?仕事内容や必要スキルについてわかりやすく解説します【SE・PG】
プログラミング
システムエンジニアとプログラマーの違いって?仕事内容や必要スキルについてわかりやすく解説します【SE・PG】

  システムエンジニアってどんな仕事? プログラマーとどう違うの?   プログラミング教育が必修化になり、SEやPGの職業が注目された今、この違いについてわからない、教えて欲しいという質問が後を絶ちません。 今回はそういった人たちのために、システムエンジニア(SE)とプログラマー(PG)という職業に...

なぜプロは有線のマウスやキーボードを選ぶのか?【有線VS無線】
雑記
Bluetooth
なぜプロは有線のマウスやキーボードを選ぶのか?【有線VS無線】

  無線が普及する現代、何故有線のマウスやキーボードは売れているのか   いろんなものが製品の進化と共に無線化している一方、有線の需要も高く、とくにプロなど上層で活躍する人は有線を選択するケースが少なくありません。 今回は、そんな有線と無線の違いや、それぞれのメリットについて解説していき...

PHPポケモン「技ポイント(PP)編」36
プログラミング
PHP,PHPポケモン,ポケモン
PHPポケモン「技ポイント(PP)編」36

技ポイント(PP)とは ポケモンではそれそれの技に使用回数が定められています。それが技ポイント(PP)と呼ばれているものです。 PP(ポケモンwiki) https://wiki.ポケモン.com/wiki/PP   技のクラスを実装した際に、それぞれにppというプロパティをもたせて回数をセットしています。これが、対象の技...

デザインとブランディングが生み出す本当の価値とは
デザイン
UCC
デザインとブランディングが生み出す本当の価値とは

  商品やHPなど様々なところで価値を提供している「デザイン」ですが、それがもつ本当の意味や価値というものは意外と知られていません。 一般的にはイメージを作る・伝えるということで要求されるものですが、突き詰めた先には人間の深層心理に働きかけるブランディングという要素が潜んでいるのです。   今...

お金を稼ぐためは理解しておきたい基本的な3要素【お客さんを増やす前に知っておかなければならないことって?】
マーケティング
お金を稼ぐためは理解しておきたい基本的な3要素【お客さんを増やす前に知っておかなければならないことって?】

  お客さんを増やすためにはどうすればいいんだろう?   ビジネスをやっている人で儲かっている一部を除けば、誰もが抱いている共通の悩みですね。 ですが、ただお客さんを増やすために安易な広告を打ったりするのは、実は危険なことなんです。   今回は「お金を稼ぐためには理解しておきた...

人に相談しても期待はするな【行動もどきって?成功する人と失敗する人の違いとは】
雑記
自己啓発
人に相談しても期待はするな【行動もどきって?成功する人と失敗する人の違いとは】

  「とにかく行動しろ」 「いろんな人に会え」 「どんどん外注しろ」   どれも覚えておきたい教訓ではありますが、その本質が見抜けなければ逆効果になります。 行動であっても、それが行動もどきになっている人は多く、人に会って人生が変わるということも受け身であれば全く意味がありません。外注に...

プレイヤー情報作成編 PHPポケモン72
プログラミング
PHP,PHPポケモン,ポケモン
プレイヤー情報作成編 PHPポケモン72

プレイヤー情報の作成 少し前よりプレイヤーをホーム画面に表示させるようにしましたが、ただイメージとして配置しただけでプレイヤー情報自体は設定していませんでした。なので、今回はプレイヤーとして持たせる必要のある情報を作成していきましょう。   クラスの作成 初代ポケモンではプレイヤー情報として閲...

HPバーアニメーション前編 サーバー側の対応 PHPポケモン 43
プログラミング
PHP,PHPポケモン,ポケモン
HPバーアニメーション前編 サーバー側の対応 PHPポケモン 43

動きのあるHPバーづくり それではデモ公開に先立ち、HPバーの作り込みをしていきたいと思います。 現在のPHPポケモンは、ダメージ計算などが終わった結果をすべて返却しているため、技選択をして次の画面に移行すると、HPが減った状態でスタートしていました。これでは、どの技でどれぐらいのダメージを与え、状態変...

カテゴリ

SEO対策 イベント デザイン ネットワーク ビジネスモデル フリーランス プログラミング マーケティング ライティング 動画編集 雑記

タグ

5G Adobe AfterEffects AI ajax amazon Animate api artisan atom Automator AWS Bluetooth CSS CVR description EC-CUBE4 ECショップ ESLint Facebook feedly foreach fortify function Google Google AdSense Honeycode htaccess HTML IEEE 802.11ax Illustrator Instagram IoT JavaScript jetstream jQuery jQuery UI keyword LAN Laravel Linux MacBook MAMP meta MLM MySQL NoCode note OS OSI参照モデル Paypal Photoshop PHP phpMyAdmin PHPポケモン PremierePro rss SEO SEO対策 Sequel Pro Skype SNS SSH Symfony TCP/IP title Toastr Trait Twig Twitter UCC V系 WAN WebSub Wi-Fi wiki Windows WordPress XAMPP xml Xserver YouTube YouTuber Zoom アーティスト アウトプット アクセス層 アニメーション アフィリエイト イーブイ インターネット インプット エンジニア オブジェクト指向 お金配り クリック単価 クリック数 コミュニケーション能力 コロナ コンサルティング サムネイル システムエンジニア スタートアップ スタイルシート スパム データベース ディープフェイク デザイナー デザイン テレワーク ナンパ ニュース ネットワークモデル ノマドワーク バナー ピカチュウ ビジネス フィード フリーランス ブロガー ブログ プログラマー プログラミング プログラミング学習 プログラミング教育 プロトコル ホームページ制作 ポケモン マークアップ マーケティング メール リモートワーク レンダリング 三井住友 三宮 仕事依頼 児童デイ 児童デイサービス 児童発達支援 公開鍵 初心者 助成金 勉強法 営業 広告 広告収入 必勝マニュアル 放課後等デイサービス 朝活 楽天 深層学習 無線LAN 独立 神戸 福祉 秘密鍵 翻訳 自己啓発 英語 見積書 計算機 認証 読書 起業 迷惑メール 配列 銀の弾丸 集客 雑学力