プログラミング

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

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

 

注目の記事

アクセス層とは – 物理的な信号の伝送【第4回 ド素人のためのネットワーク講座】
ネットワーク
TCP/IP,Wi-Fi,アクセス層,無線LAN
アクセス層とは – 物理的な信号の伝送【第4回 ド素人のためのネットワーク講座】

  第4回のド素人のためのネットワーク講座は アクセス層の物理的な役割について です。   実際にデータを送る際にはどのような信号が送られて、どういったことに影響を受けているのかを解説していきます。     信号を伝達する   TCP/IPモデルの第一層(レイヤー1)のアクセス層が担っている役割の...

「数字を上げる」必勝マニュアル 〜再生回数・フォロワー・PV数〜
マーケティング
YouTuber,ブロガー,必勝マニュアル,自己啓発
「数字を上げる」必勝マニュアル 〜再生回数・フォロワー・PV数〜

  「継続は力なり」   色んな場面で言われます。何事も地道な努力が大事です。 しかし、地道な努力というのは成果が見えづらく、反応が得にくいことも確かです。   運良く勢いに乗れて、常に努力のし易い環境にいることで伸びていく人の確かにいます。 しかし、ほとんどの人がそうはいきません。...

記事を書いてもブログが伸びないのは何故?SEOの評価を下げてしまう絶対にダメな3つのこと【知らない内に損してます】
SEO対策
SEO,ブログ
記事を書いてもブログが伸びないのは何故?SEOの評価を下げてしまう絶対にダメな3つのこと【知らない内に損してます】

  1年ブログを継続したけど、全くPVが伸びない・・・   ブログを育てるためには、毎日または定期的な更新が必ず必要ですが、それでも思うように伸びないという人は、実は他に致命的にな原因があることがほとんどです。 今回はそういった「努力しても結果が出ない」と悩んでいるブロガーやブログ運営者に...

【無料】早起きをして神戸へ行こう!「為になる雑談朝活」
イベント
三宮,朝活,神戸
【無料】早起きをして神戸へ行こう!「為になる雑談朝活」

  朝活を実施することになりましたので、その目的な概要をまとめました。 神戸三宮での開催を予定しておりますので、もしお近くにお住まいの方で日時が会いましたらご参加ください。 土日祝辺りで週1日程度の不定期開催を予定しています。学びにつながる、けど参加しやすい雑談形式ということを主としています...

EC-CUBE4内にWordPressを設置(共存)する方法
SEO対策,プログラミング
EC-CUBE4,PHP,Symfony,Twig,WordPress
EC-CUBE4内にWordPressを設置(共存)する方法

今回使用したバージョン EC-CUBE4.0.5 WordPress5.6 ※EC-CUBE4、WordPressのカスタマイズは自己責任でお願いします。また、現在稼働中のサイトで試す際には、必ずバックアップを取ってから行なうようにしてください。   EC-CUBE4とは ネットショップを作るとなれば、様々なサービスが出回っ...

PHPポケモン「技クラス実装編」14
プログラミング
PHP,PHPポケモン,ポケモン
PHPポケモン「技クラス実装編」14

  前回せっかくBootstrapを使って見た目を整えたにも関わらず、ビューポートの記述が抜けているという凡ミスが発覚したので修正しています。 サーセン。   今回のPHPポケモンでは本格的な技システムを実装していきます。技システムが整えば、皆さん期待のバトルシステムも間近です。セキュリティ面やファイル構成...

初心者必見!仕事がデキる人のIllustrator活用術3選
デザイン
Adobe,Illustrator,サムネイル
初心者必見!仕事がデキる人のIllustrator活用術3選

  仕事の速い人の大半は、「効率の良い方法」や「便利な使い方」を知っていて使いこなしています。これは、デザインの現場でも同じです。 今回はその中でもIllustratorで知っておくと便利な機能を3つご紹介します。この3つを押さえておくだけで、作業効率は格段にアップするので、これからソフトの使い方を覚...

モンスターボール作成編 PHPポケモン 79
プログラミング
PHP,PHPポケモン,ポケモン
モンスターボール作成編 PHPポケモン 79

モンスターボールとは ポケモンのゲームにとっての楽しみの1つである「バトル」はある程度実装できてきましたが、もう一つ無くてはならない楽しみがあります。それが「ポケモンを集める」というコレクター要素です。 そして、ポケモンを捕まえるために欠かせない道具の1つが「モンスターボール」です。  モンス...

カテゴリ

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 独立 神戸 福祉 秘密鍵 翻訳 自己啓発 英語 見積書 計算機 読書 起業 迷惑メール 配列 銀の弾丸 集客 雑学力