プログラミング

トークン認証とサニタイズ編 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やフレームワークを使って管理することをオススメします。特にシステム要素が強いものや規模が大きいものであれば尚更です。

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

 

注目の記事

食に関する考察 自炊と節約は成立するのか?
雑記
食に関する考察 自炊と節約は成立するのか?

スキル関係の記事が多めの当ブログですが、今回は「食」をテーマに触れていきます。突き詰めていけば食も立派なスキルであり、奥が深いものです。更に、生活の基盤となるため多くの人がこの問題に直面することであり、健康とも直結するため仕事などにおけるパフォーマンスにも大きく関係します。   これから一人暮ら...

ノーコード開発で将来プログラミングは不要?我々は何を学ぶべきなのか
プログラミング
amazon,AWS,Honeycode,NoCode,プログラミング学習
ノーコード開発で将来プログラミングは不要?我々は何を学ぶべきなのか

  近年、プログラミングの需要は増え勢いを増していますが、それ以上にノーコードというソースコードを打たなくでもアプリ開発ができるというソフトウェアやプラットフォームが普及してきました。 これにより一部の期待感とは別に、プログラマーという職業は今後不要になるのではないかと危機感を抱く人が出て...

売れるECサイトになるために必要な3つの戦略
マーケティング
ECショップ,コンサルティング
売れるECサイトになるために必要な3つの戦略

  ECサイトで全く売れない・・・   ネットショップのオープンが手軽で安価になり、クレジット決済も主流の今ECサイトを立ち上げるお店も増えてきました。 しかし期待感とは裏腹に、思ったような売上が出なかったり、お客さんが1人も獲得できていないケースも少なくありません。   今回はそういった「...

目先の利益に気をつけろ!貧乏ビジネスという落とし穴
フリーランス
目先の利益に気をつけろ!貧乏ビジネスという落とし穴

  目先の利益を求めてしまい、来たるべきビジネスチャンスに対応できないというケースは貧乏ビジネスに陥る大きな要因になります。また、相手が下す評価に左右されてしまうことも、自らの評価を下げてしまったり、見積もりを作る上でも大きく影響を及ぼしてしまいます。   今回は「目先の利益に気をつけろ!貧...

PHPポケモン「バトルシステム編〜HP計算〜」26
プログラミング
PHP,PHPポケモン,ポケモン
PHPポケモン「バトルシステム編〜HP計算〜」26

HP計算 これまでに実装したダメージ計算ですが、計算はできていてもお互いに相手ポケモンのHPを削ることはできませんでした。なので、今回は実際のバトルのように、HPに対してダメージを与えるという仕組みを作成していきます。   残りHP ここで必要になるのが「残りHP」という概念です。今までステータス上のHP...

わざマシン編 習得 PHPポケモン105
プログラミング
PHP,PHPポケモン,ポケモン
わざマシン編 習得 PHPポケモン105

わざマシンによる技習得 前回、わざマシンのアイテムクラスを作成したので、今回は実際に使用するまでの処理を作成していきます。 まずは、アイテムクラスに使用時の処理(effectsメソッド)を増設しましょう。   わざマシン01(/Classes/Item/ItemTM01.php) <?php require_once(root_path('Classes').'...

手っ取り早く情報強者になる簡単な方法
雑記
アウトプット,インプット,ニュース
手っ取り早く情報強者になる簡単な方法

  ニュースや情報番組、討論番組をみると、出演者の方々の情報量の多さに圧倒されることがあります。 また、ユニークな考え方に共感を得る人も多いでしょう。   どうやって、情報を仕入れているのか? なぜそんなことまで知っているのか?   メディアで取り上げられているような人や、活躍している人の多く...

迷惑メールはなぜ届く?amazonや楽天を騙る悪質メールへの対処法とは
ネットワーク
amazon,スパム,メール,楽天
迷惑メールはなぜ届く?amazonや楽天を騙る悪質メールへの対処法とは

「いきなり迷惑メールが届くようになった」 「Amazonや楽天を装ったメールはどうやって見分ければいいの?」   知らない人からいきなり連絡が届いたり、登録もしていないようなサービスからメールが届けば、それは詐欺メールかも知れません。 スマホやパソコンが一般的に普及して、ネットでの買い物やサービスを利用...

カテゴリ

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

タグ

5G Adobe AfterEffects AI ajax amazon Animate api artisan atom Automator AWS Bluetooth CSS CVR description EC-CUBE4 ECショップ ESLint Facebook feedly foreach function Google Google AdSense Honeycode htaccess HTML IEEE 802.11ax Illustrator Instagram IoT JavaScript 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 独立 神戸 福祉 秘密鍵 翻訳 自己啓発 英語 見積書 計算機 読書 起業 迷惑メール 配列 銀の弾丸 集客 雑学力