前回実装したオートローダーの使い方が盛大に間違っていたので、今回その間違いの説明をしながら、正しい実装方法をご紹介します。
申し訳ありません。(誠意)
オートロードについて(再)
必要なタイミングで必要なファイルをrequireまたはincludeするあれです。
前回spl_autoload_registerの関数を使用した簡易オートローダーを作成したのですが、呼び出していないのに必要なクラスを呼び出してインスタンス化されているという現象がタイプの取得部分で起こっていたため、再度公式(PHP.net)や他の紹介記事を参考にしたところ、この関数は一度登録すれば毎回呼び出す必要が無いということが発覚しました。
※よくみればregisterってなってますからね・・・
ということで、前回トレイトで作成したオートローダーはなかったことにしてしっかりと理解した上で作り込んでいきます。
オートローダーの作成
1度実行すれば良いのであれば、トレイトである必要はありません。なのでクラスとして作成します。
オートローダー(/Classes/AutoLoader.php)
<?php
class AutoLoader
{
/**
* 検索するクラスが格納されているフォルダ
* @return void
*/
private $folders = [
'Move', 'Pokemon', 'Type',
];
/**
* オートローダー
*
* @return void
*/
public function __construct()
{
spl_autoload_register([$this, 'autoLoader']);
}
/**
* コールバック用メソッド
*
* @return void
*/
private function autoLoader($class_name)
{
// クラス名からファイルを検索
foreach($this->folders as $folder){
$path = __DIR__ . '/../Classes/'.$folder.'/'.$class_name.'.php';
if(file_exists($path)){
// 見つかった場合は読み込み実行
require $path;
}
}
}
}
まず、プロパティに注目しましょう。
/**
* 検索するクラスが格納されているフォルダ
* @return void
*/
private $folders = [
'Move', 'Pokemon', 'Type',
];
技(Move)、ポケモン(Pokemon)、タイプ(Type)のClassesに設置した3フォルダを指定しています。もしクラスが見つからなかった際は、ここから探してくれます。もしアイテムなどが増えた際は、ここに必要フォルダを増やしていきましょう。
次にコンストラクタを見てみましょう。
/**
* オートローダー
*
* @return void
*/
public function __construct()
{
spl_autoload_register([$this, 'autoLoader']);
}
spl_autoload_registerは処理の前半に1度だけ呼び出しておけば良いので、オートローダーのクラスをインスタンス化した時点で動くようにしました。中にはメソッドを用意して、インスタンス化→メソッド実行という手順をとっているものもありましたが、今の所一括で出来たほうが良いだろうという判断なのでこの仕様にしています。
もし、セキュリティ面や挙動がおかしくなるなど問題が発生したら、適宜修正していきます。
コールバック関数は、前回のように無名関数にせず、可視性がよくなるためにもメソッドへ分けて作成しました。
/**
* コールバック用メソッド
*
* @return void
*/
private function autoLoader($class_name)
{
// クラス名からファイルを検索
foreach($this->folders as $folder){
$path = __DIR__ . '/../Classes/'.$folder.'/'.$class_name.'.php';
if(file_exists($path)){
// 見つかった場合は読み込み実行
require $path;
break;
}
}
}
読み込まれていないクラスを呼び出そうとしたタイミングで、spl_autoload_registerは起動して、コールバック関数の第1引数に、そのクラス名が渡されます。以下の例を見てみましょう。
- オートローダーをインスタンス化
→ spl_autoload_registerが登録される
- ピカチュウのクラスを呼び出そうとする
→ ピカチュウのクラスはincludeまたはrequireされていない
- spl_autoload_registerが動き、コールバック関数(autoLoader)の第1引数($class_name)にPikachu(ピカチュウのクラス名)が渡される
上記の流れになります。
そして、コールバック関数内ではforeachを使ってプロパティに設定したフォルダから順番にピカチュウのクラスを検索させています。
これだけです。一度登録していれば、見つからないクラスが呼び出された時点で勝手に探してくれるというのが、前回の説明で漏れていた(というより理解できていなかった)部分です。composerの素晴らしさがわかると同時に、頼り切りだと知らないことがいっぱいだということを改めて教えられました。
namespaceを使用していない関係上、一発で該当フォルダへたどり着くことができないため、無駄な動きは発生していますが、膨大な量ではないので今回は無視します。
class_existsの挙動について
次に、class_existsの関数について見ていきましょう。これが今回良くも悪くも気づきをくれました。
class_exists(PHP.net)
第2引数でautoloadをデフォルトでコールするかしないかを指定できます。省略すればtrueになるのでコールしてくれます。
最初は関係ないと思ってスルーしていましたが、オートローダーを実装した途端これが大きな意味を成してきました。
技の習得や、進化をする際にclass_existsを何度か使用しています。これは、クラスが不正なものでは無いか(または名称間違いになっていないか)を判別するぐらいで使用していたのですが、オートローダーを導入した関係上、基本的にはクラスは存在しない(読み込まれていない状態)ものをチェックすることになります。その際に、それをオートロードを実行してからチェックするかどうかというのが、class_existsの第2引数の役割です。
通常のclass_existsを使う場合、第2引数にはfalseを指定することが多いです。その理由は、基本的にはクラスは読み込んでいることが前提だからです。しかし、今回のPHPポケモンの現段階で使用しているclass_existsは、基本的に読み込んでいないクラスを探してもらうため、第2引数は省略(true)のままで進めていきます。
では、実際に修正箇所を見ていきましょう。
コントローラー(/Classes/Controller.php)
/**
* @return void
*/
public function __construct($post, $session)
{
// オートローダーの起動
new AutoLoader();
まず、コントローラーの最初にオートローダーをインスタンス化します。これで、spl_autoload_registerが登録されたことになります。
※AutoLoader.phpはrequireしておいてください
インスタンス化トレイト(/Traits/InstanceTrait.php)
<?php
trait InstanceTrait
{
/**
* インスタンス化関数
* @param string $class_name
* @return object
*/
protected function getInstance($class_name)
{
// 存在チェックをして読み込み
if(class_exists($class_name)){
return new $class_name();
}
}
}
インスタンス化用のトレイトも、前回まではポケモンや技など種類分けをしていましたが、その必要が無くなったので簡潔なものに戻しています。
あとは、インスタンス化したい際に使用すれば、class_existsで勝手にクラスを探して読み込んでくれるという仕様です。
ポケモンデータの引き継ぎ
これでタイプや技に関しては上手く行きましたが、ポケモンクラスで問題が発生しました。
今まではコントローラーで各ポケモンのクラスを読み込んでいたのですが、これをオートローダーに変更したところメソッド等が呼び出せなくなりました。原因を確認したところ、セッション内に以下のようなものが入力されていました。
__PHP_Incomplete_Class Object
そもそも、インスタンスはセッションで渡すべきでは無いそうです。よくよく考えたら、セキュリティの関係からも当たり前と納得です。なので、こちらも修正します。
いままでは、ポケモンをインスタンス化する際に「捕まえた」「進化前」だけを判定していました。ですが、前画面から同じポケモンの引き継ぎという判定を追加しなければいけません。必要なデータだけを配列としてセッションに渡し、コントローラーの最初でその引き継ぎデータ(配列)を使って新しくポケモンをインスタンス化します。
ではまずポケモンクラスの修正版から見ていきましょう。
ポケモンクラス(/Classes/Pokemon.php)
<?php
require_once(__DIR__.'/../Traits/ResponseTrait.php');
require_once(__DIR__.'/../Traits/InstanceTrait.php');
require_once(__DIR__.'/../Traits/Pokemon/SetTrait.php');
require_once(__DIR__.'/../Traits/Pokemon/GetTrait.php');
// ポケモン
abstract class Pokemon
{
use ResponseTrait;
use InstanceTrait;
use SetTrait;
use GetTrait;
/**
* ニックネーム
* @var string(min:1 max:5)
*/
protected $nickname;
/**
* 現在のレベル
* @var integer(min:2 max:100)
*/
protected $level;
/**
* 覚えている技
* @var array(min:1 max:4)
*/
protected $move = [];
/**
* 経験値
* @var integer
*/
protected $exp;
/**
* 個体値
* @var array(value min:0 max:31)
*/
protected $iv = [
'HP' => null,
'Attack' => null,
'Defense' => null,
'SpAtk' => null,
'SpDef' => null,
'Speed' => null,
];
/**
* 努力値
* @var array
*/
protected $ev = [
'HP' => 0,
'Attack' => 0,
'Defense' => 0,
'SpAtk' => 0,
'SpDef' => 0,
'Speed' => 0,
];
/**
* インスタンス作成時に実行される処理
*
* @param object|array|null
* @return void
*/
public function __construct($before=null)
{
switch (gettype($before)) {
/**
* 捕まえた際の処理
* @var null $before
*/
case 'NULL':
$this->setLevel();
$this->setDefaultExp();
$this->setDefaultMove();
$this->setIv();
$this->setMessage($this->name.'をゲットした', 'success');
break;
/**
* 前の画面からの引き継ぎ
* @var array $before
*/
case 'array':
$this->takeOverAbility($before);
break;
/**
* 進化した際の処理
* @var object $before
*/
case 'object':
// 進化前のポケモンと一致しているかチェック
if(is_a($before, $this->before_class ?? null)){
$this->takeOverAbility($before);
// メッセージの引き継ぎ
$this->setMessage($before->getMessages());
$this->setMessage($this->name.'に進化した', 'success');
$this->checkMove();
}
break;
}
}
/**
* レベルアップ処理
*
* @return void
*/
protected function actionLevelUp()
{
// レベルアップ
$this->level++;
$this->setMessage($this->getNickName().'のレベルは'.$this->level.'になった!', 'success');
// 現在のレベルで習得できる技があるか確認
$this->checkMove();
}
/**
* 進化
*
* @return Classes\Pokemon\$after_class
*/
protected function evolve()
{
if(class_exists($this->after_class ?? null)){
$pokemon = new $this->after_class($this);
// 進化後のインスタンスを返却
return $pokemon;
}else{
$this->setMessage('このポケモンは進化できません', 'error');
}
}
/**
* 現在のレベルで覚えられる技があるか確認する処理
*
* @var integer
*/
protected function checkMove()
{
// レベルアップして覚えられる技があれば習得する
$level_move_keys = array_keys(array_column($this->level_move, 0), $this->level);
foreach($level_move_keys as $key){
$move_class = $this->level_move[$key][1];
// 覚えようとしている技(クラス)が存在かつ重複していないか
if(class_exists($move_class) && !in_array($move_class, $this->move, true)){
// 技クラスをセット
$this->setMove($move_class);
// インスタンス化
$move = new $move_class();
$this->setMessage($move->getName().'を覚えた!', 'success');
}
}
}
/**
* 現在インスタンスを出力
*
* @return array
*/
public function export()
{
return [
'class_name' => get_class($this), # クラス名
'nickname' => $this->nickname, # ニックネーム
'level' => $this->level, # レベル
'ev' => $this->ev, # 努力値
'iv' => $this->iv, # 個体値
'exp' => $this->exp, # 経験値
'move' => $this->move, # 技
];
}
/**
* 進化時の能力引き継ぎ処理
*
* @param object $before 進化前
* @return void
*/
protected function takeOverAbility($before)
{
if(is_object($before)){
$this->nickname = $before->nickname; # ニックネーム
$this->level = $before->level; # レベル
$this->ev = $before->ev; # 努力値
$this->iv = $before->iv; # 個体値
$this->exp = $before->exp; # 経験値
$this->move = $before->move; # 技
}elseif(is_array($before)){
$this->nickname = $before['nickname']; # ニックネーム
$this->level = $before['level']; # レベル
$this->ev = $before['ev']; # 努力値
$this->iv = $before['iv']; # 個体値
$this->exp = $before['exp']; # 経験値
$this->move = $before['move']; # 技
}
}
}
コンストラクタ内でswitchを使い、引数で受け取ったbeforeの型をgettype関数を使ってチェックしています。
gettype(PHP.net)
NULL → ゲット
array → 引き継ぎ
object → 進化
上記3つの判定になります。ただ、分岐が多くなりすぎるのも良くないので、そろそろメソッド分けする必要がありそうですが、一旦はこのまま押し通します。
次に新しく追加したexportというメソッドを見てみましょう。
/**
* 現在インスタンスを出力
*
* @return array
*/
public function export()
{
return [
'class_name' => get_class($this), # クラス名
'nickname' => $this->nickname, # ニックネーム
'level' => $this->level, # レベル
'ev' => $this->ev, # 努力値
'iv' => $this->iv, # 個体値
'exp' => $this->exp, # 経験値
'move' => $this->move, # 技
];
}
これは、現在のポケモンの引き継ぎ用情報を出力するためのメソッドです。これを使い、セッションに必要なデータを渡します。引き継ぎ後はクラスを呼び出す必要があるため、クラス名をget_classで取得して配列に格納しています。
変更が加えられた、能力の引き継ぎメソッド(takeOverAbility)についても見てみましょう。
/**
* 進化時の能力引き継ぎ処理
*
* @param object $before 進化前
* @return void
*/
protected function takeOverAbility($before)
{
if(is_object($before)){
$this->nickname = $before->nickname; # ニックネーム
$this->level = $before->level; # レベル
$this->ev = $before->ev; # 努力値
$this->iv = $before->iv; # 個体値
$this->exp = $before->exp; # 経験値
$this->move = $before->move; # 技
}elseif(is_array($before)){
$this->nickname = $before['nickname']; # ニックネーム
$this->level = $before['level']; # レベル
$this->ev = $before['ev']; # 努力値
$this->iv = $before['iv']; # 個体値
$this->exp = $before['exp']; # 経験値
$this->move = $before['move']; # 技
}
}
前回までは進化の際にしか能力値を引き継いでいませんでしたが、今回からは画面移管でも引き継ぐ必要があるので、配列で受け取った場合の処理を追加しました。これで、どちらのパターンでも同じメソッドを使った能力の引き継ぎが可能です。
それではコントローラーの修正箇所(コンストラクタ)を見ていきましょう。
コントローラー(/Classes/Controller.php)
/**
* @return void
*/
public function __construct()
{
$post = $_POST;
$session = $_SESSION;
// オートローダーの起動
new AutoLoader();
/**
* 初期画面
*/
if(empty($post)){
$_POST = [];
$_SESSION = [];
return;
}
/**
* ポケモンが選択された
*/
if(isset($post['pokemon'])){
$this->checkPokemon(htmlspecialchars($post['pokemon']));
return;
}
/**
* アクションが実行された
*/
if(isset($post['action']) && class_exists($session['pokemon']['class_name'] ?? null)){
// 引き継いだデータを引数にポケモンをインスタンス化
$this->pokemon = new $session['pokemon']['class_name']($session['pokemon']);
// アクションメソッドの実行
$this->action(htmlspecialchars($post['action']), htmlspecialchars($post['param'] ?? null));
}
}
前回までは引数でpostとsessionを受け取っていましたが、正直グローバルでとっても問題なさそうなので変更しました。ただ、そのままは使用せずに一度変数に格納してから使っています。
大きく変更されたのは、アクションが実行された際の処理です。今まではそのままpokemonのプロパティに渡していましたが、class_existsでチェックしてからインスタンス化するという手順を踏んでいます。
あと追加で、申し訳程度にhtmlspecialcharsを使ってサニタイズしています。
コントローラーの仕様に合わせて出力画面も修正します。
出力画面(/index.php)
<?php
require_once(__DIR__.'/Classes/Controller.php');
require_once(__DIR__.'/Resources/Lang/Translation.php');
session_start();
$controller = new Controller();
$action_path = '/';
?>
<!DOCTYPE html>
<html lang="jp" dir="ltr">
<head>
<meta charset="utf-8">
<title>PHPポケモン</title>
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<link rel="stylesheet" href="Resources/Assets/css/style.css">
</head>
<body>
<header>
<div class="container">
<section>
<div class="row">
<div class="col-12">
<h1>PHPポケモン</h1>
</div>
</div>
</section>
</div>
</header>
<main>
<div class="container">
<?php if(isset($controller->pokemon)): ?>
<section>
<div class="row">
<div class="col-12 col-sm-6 col-md-4">
<?php # 詳細 ?>
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th scope="col" colspan="2">詳細</th>
</tr>
</thead>
<tbody>
<?php foreach($controller->pokemon->getDetails() as $key => $val): ?>
<tr>
<th scope="row" class="w-50"><?=transJp($key)?></th>
<td><?=$val?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="col-12 col-sm-6 col-md-4">
<?php # ステータス ?>
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th scope="col" colspan="2">ステータス</th>
</tr>
</thead>
<tbody>
<?php foreach($controller->pokemon->getStats() as $key => $val): ?>
<tr>
<th scope="row" class="w-50"><?=transJp($key)?></th>
<td><?=$val?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="col-12 col-sm-6 col-md-4">
<?php # 覚えている技 ?>
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th scope="col">覚えている技</th>
<th scope="col">タイプ</th>
<th scope="col">PP</th>
</tr>
</thead>
<tbody>
<?php foreach($controller->pokemon->getMove() as $move): ?>
<tr>
<th scope="row" class="w-50"><?=$move->getName()?></th>
<td><?=$move->getType()->getName()?></td>
<td><?=$move->getPp()?>/<?=$move->getPp()?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
<?php endif; ?>
<section>
<div class="row">
<div class="col-12 col-sm-6 mb-5">
<h2 class="mb-3">メソッドの実行</h2>
<?php if(is_object($controller->pokemon ?? null)): ?>
<?php $_SESSION['pokemon'] = $controller->pokemon->export(); # ポケモンの情報をセッションに格納 ?>
<?php include('Resources/Partials/Forms/change_nickname.php'); # ニックネームの変更?>
<?php include('Resources/Partials/Forms/add_exp.php'); # 経験値の取得 ?>
<?php include('Resources/Partials/Forms/reset.php'); # リセット ?>
<?php else: ?>
<?php include('Resources/Partials/Forms/select_pokemon.php'); ?>
<?php endif; ?>
</div>
<div class="col-12 col-sm-6 mb-5">
<h2 class="mb-3">実行結果</h2>
<div class="result-box border p-3 mb-3">
<?php foreach($controller->getMessages() as list($msg, $status)): ?>
<?php if($status == 'error') $class = 'text-danger'; ?>
<?php if($status == 'success') $class = 'text-success'; ?>
<p class="<?=$class ?? ''?>"><?=$msg?></p>
<?php endforeach; ?>
<?php if(!empty($controller->getResponses())): ?>
<pre><?php var_export($controller->getResponses()); ?></pre>
<?php endif; ?>
</div>
</div>
</div>
</section>
</div>
</main>
</body>
</html>
日本語化の実装(おまけ)
先程のindex.phpにも記述していますが、一部分だけ日本語化を実装しました。とは言っても、英語をキーとした日本語を配列として用意し、それを関数で呼び出してチェックしているだけです。
翻訳ファイル(/Resources/Lang/Translation.php)
<?php
function transJp($str)
{
$array = [
'attack' => 'こうげき',
'defense' => 'ぼうぎょ',
'spatk' => 'とくこう',
'spdef' => 'とくぼう',
'speed' => 'ぼうぎょ',
'name' => '正式名称',
'nickname' => 'ニックネーム',
'type' => 'タイプ',
'level' => 'レベル',
'exp' => '経験値',
'nextlevel' => '次のレベルまで',
'physical' => '物理',
'special' => '特殊',
'status' => '変化',
];
// 小文字変換して配列から取得、存在しなければそのまま返却
return $array[mb_strtolower($str)] ?? $str;
}
今まで日本語キーだった部分を英語に置き換え、それに合わせてtransJpという関数を呼び出す仕様です。もし技やポケモンの名前も、わざわざインスタンス化してgetNameをするのが面倒であれば、同じように用意して関数で置き換えるという方法をとっても構いません。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは、前回実装したオートローダーの不備を修正し、正しい使い方をご紹介しました。
ぜひオートローダーを使う人は、しっかり公式マニュアルや書籍を読み込んでから実装するようにしておきましょう。