プログラミング

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

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

 

注目の記事

お金を生む仕組みを理解しよう【知っておきたい3つのポイント】
マーケティング
お金を生む仕組みを理解しよう【知っておきたい3つのポイント】

  どんなビジネスが儲かるのか・・・   一度はこういった悩みを持ったことがあるのではないでしょうか。 それを知るためには、どういった要素がお金を生むために作用しているのかを理解しておく必要があります。 今回は「お金を生む仕組み」について、知っておいてためになる基礎的な部分をわかりやすく...

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

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

20代起業家が教える「やっといて良かった」3つのコト
雑記
起業
20代起業家が教える「やっといて良かった」3つのコト

  「起業するために何を準備すべき?」 「やっておいて良かったことはありますか?」   独立や起業をしようと志している人のほとんどが、こういった質問を投げかけてきます。自分も同じような悩みを持った立場の時には似た質問をしていたので、その真意はよくわかります。   今回はそんな悩みを抱えて...

なぜ(・・? あなたはMacを使うのか?
雑記
AWS,Linux,MacBook,OS,Windows
なぜ(・・? あなたはMacを使うのか?

  あなたが使っているPCはなんですか?   たまに聞かれることがあります。 そこで「Macを使っています」と答えると「何故?」という次の問いが返ってくることがあります。   あなたがもしMacを使っているのであれば、このときになんと答えますか?   今回のテーマは、「何故Macを使っているの?」と...

フリーランスのための値段交渉術!案件はこうやって見極めろ
フリーランス
フリーランスのための値段交渉術!案件はこうやって見極めろ

  「これもっと安くならない?」 「他の人はこれぐらいの金額でやってくれるよ」   こういった値切り交渉を受けたことがある人もいるのではないでしょうか?技術を提供するフリーランスであれば、金額を落とすということは自分の価値を下げているということを理解しておかなければなりません。 値段交渉...

PHPポケモン「わざ編〜チャージ技の実装〜」34
プログラミング
PHP,PHPポケモン,ポケモン
PHPポケモン「わざ編〜チャージ技の実装〜」34

チャージ技とは ポケモンの技は数多く存在していて、その中でも特別な処理が必要なものがいくつかあります。その1つが「チャージ技」です。 ※チャージ技とはポケモン上で用いられている用語ではありません   現在実装している初代御三家+ピカチュウの初代レベルアップ技の中では以下の2つがあります。 ロ...

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

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

賢い集客でボロ儲け!?仕事や案件に困らない基本戦略・3選
マーケティング
賢い集客でボロ儲け!?仕事や案件に困らない基本戦略・3選

  「仕事はどうすればもらえるの?」 「集客って何すればいいかわからない」 「告知ってどんな媒体をどう使えばいいの?」   フリーランスで仕事の取り方がわからず苦戦している人や、サービスや商品は作ったものの集客の方法がわからず悩んでいる人のほとんどが、集客のテクニックばかりに目線がいってしま...

カテゴリ

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