リュックの作成
前回はフレンドリィショップへ商品を並べ、計算機を作成するところまで作成しました。ですが、商品が購入できたとしても、それを保管しておくためのスペースがなければ意味がありません。
なので、プレイヤー情報に対してアイテムを格納できるように機能拡張をしましょう。
プレイヤークラス(/Classes/Player.php)
<?php
$root_path = __DIR__.'/..';
// トレイト
require_once($root_path.'/App/Traits/Class/Player/ClassPlayerItemTrait.php');
require_once($root_path.'/App/Traits/Class/Player/ClassPlayerBadgeTrait.php');
require_once($root_path.'/App/Traits/Class/Player/ClassPlayerMoneyTrait.php');
/**
* プレイヤー情報
*/
class Player
{
use ClassPlayerItemTrait;
use ClassPlayerBadgeTrait;
use ClassPlayerMoneyTrait;
--省略
/**
* 持ち物
* [[class => string, count => int|null], ...]
* @var array
*/
protected $items = [];
※メソッドが多くなってきたので、それぞれをトレイト分けすることで管理しました
アイテムは、配列として格納します。構成は、アイテムの種類1つに対して配列としてクラス名(class)と所有数(count)を持たせました。オブジェクトとして格納してしまうと、サニタイズの処理に負荷がかかるのと、クラス変更による反映が難しくなるため、上記の構成にしています。
カテゴリ分けして整頓
次に、カテゴリ分けについてです。リュック内で見やすくするためにも、アイテムに設定した「ボール」や「回復」などのように分けて表示させなければいけません。ですが、アイテムプロパティの配列内を、さらにカテゴリで多次元化すれば、管理する上では複雑化してしまいます。なので、カバンとして取得する際にカテゴリ分け・インスタンス化する機能を持たせたメソッドを用意します。
アイテム管理用トレイト(/App/Traits/Class/Player/ClassPlayerItemTrait.php)
<?php
trait ClassPlayerItemTrait
{
/**==================================================================
* どうぐ
==================================================================**/
/**
* かばんの生成
* @return array
*/
public function getBag(): array
{
// carry初期値
$initial = array_map(function(){
return [];
}, array_flip(config('item.categories')));
// カテゴリ分けした配列を返却
return array_reduce($this->items, function($carry, $row){
$item = new $row['class'];
$carry[$item->getCategory()][] = [
'item' => $item,
'count' => $row['count'],
];
return $carry;
}, $initial);
}
}
配列をカテゴリ分けするために、array_reduceという関数を使用します。
array_reduce(PHP.net)
第1引数にカテゴリ分けしたい配列、第2引数にコールバック関数、第3引数にカテゴライズの初期配列を設定することができます。第3引数は、コールバック関数の第1引数の初期値となり、コールバック関数の第2引数ではループさせている配列の値(第1引数)が格納されます。
コールバック関数が含まれるものは、実際に使ってみなければその挙動はわかりにくいため、処理順に見ていきましょう。
// carry初期値
$initial = array_map(function(){
return [];
}, array_flip(config('item.categories')));
まずはカテゴリ分けするための初期値として、カテゴリ名がキー、要素が空の配列を用意します。config(‘item.categories’)の取得結果は以下のようになっています。
[
'general', 'health', 'ball', 'machine', 'important'
]
array_mapの入力配列として使用する際に、array_flipでconfigから受け取ったカテゴリ名のキーと要素を逆転させています。
array_flip(PHP.net)
array_flip後の配列状態は以下の通りです。
[
'general' => 0 , 'health' => 1, 'ball' => 2, 'machine' => 3, 'important' => 4
]
キー(添字)と値が入れ替わっていることがわかりますね。出来上がった配列をarray_mapを使って、値をから配列に変換した結果が以下の通りです。
[
'general' => [] , 'health' => [], 'ball' => [], 'machine' => [], 'important' => []
]
出来上がった配列に対して、該当するアイテムをarray_reduceを使ってそれぞれ割り振っていきます。
// カテゴリ分けした配列を返却
return array_reduce($this->items, function($carry, $row){
$item = new $row['class'];
$carry[$item->getCategory()][] = [
'item' => $item,
'count' => $row['count'],
];
return $carry;
}, $initial);
$carryには先程作成したカテゴリ配列($initial)、$rowにはアイテム([class, count])が入っています。まず初めにアイテムをインスタンス化、そこからクラス名を取得して$carryの該当配列へ、アイテムのインスタンスと個数を配列として格納しています。
これで、array_reduceの返り値がカテゴライズされたアイテム配列になるので、そのままループして出力すればカテゴライズされた状態が簡単に作り出せます。
ただカテゴライズするだけであれば、$carryの初期値は設定する必要がありません。ですが、リュックには対象カテゴリのアイテムが存在していなくてもカテゴリ名は表示したかったので、初期値を用意してからループさせました。
あとは、ポケモンやプレイヤー同様にアイテム用のモーダルを作成して、上記のメソッドを使って出力していきます。
アイテムモーダル(/Resources/Partials/Home/Modales/item.php)
<!-- Modal -->
<?php $bag = $player->getBag(); ?>
<div class="modal fade" id="item-modal" tabindex="-1" role="dialog" aria-labelledby="item-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="item-modal-title">どうぐ</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body my-2">
<?php # タブ ?>
<nav class="nav nav-pills nav-justified btn-group mb-3" id="item-modal-tab">
<?php $cnt = 0; ?>
<?php foreach($bag as $category => $items): ?>
<a class="btn btn-outline-secondary nav-item nav-link <?php if(!$cnt) echo 'active'; ?>" id="item-modal-<?=$category?>-tab" data-toggle="tab" href="#item-modal-<?=$category?>" role="tab" aria-controls="item-modal-<?=$category?>" aria-selected="true">
<img src="/Assets/img/item/category/<?=$category?>.png" alt="<?=transJp($category, 'item');?>">
</a>
<?php $cnt++; ?>
<?php endforeach; ?>
</nav>
<?php # コンテンツ ?>
<div class="tab-content" id="item-modal-tab-content">
<?php $cnt = 0; ?>
<?php foreach($bag as $category => $items): ?>
<div class="tab-pane fade show <?php if(!$cnt) echo 'active'; ?>" id="item-modal-<?=$category?>" role="tabpanel" aria-labelledby="item-modal-<?=$category?>">
<h6 class="font-weight-bolder mb-2"><?=transJp($category, 'item');?></h6>
<div class="bg-light p-3 mb-2 overflow-auto" style="height:120px;">
<h6 id="item-modal-<?=$category?>-name" class="font-weight-bolder"></h6>
<hr>
<p class="mb-0 small" id="item-modal-<?=$category?>-description"></p>
</div>
<div class="bg-light p-3 overflow-auto" style="height:300px;">
<?php if(empty($items)): ?>
<p class="mb-0">1つも持っていません</p>
<?php else: ?>
<table class="table table-sm table-hover table-selected table-bordered bg-white mb-0">
<tbody>
<?php foreach($items as $item): ?>
<tr data-description="<?=$item['item']->getDescription()?>"
data-name="<?=$item['item']->getName()?>"
data-category="<?=$category?>"
class="item-row">
<td class="w-75">
<img src="/Assets/img/item/class/<?=get_class($item['item'])?>.png" alt="<?=$item['item']->getName()?>" class="mr-1" />
<?=$item['item']->getName()?>
</td>
<td class="w-25 text-right"><?=$item['count']?> 個</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php $cnt++; ?>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
アイテムを選択すると、上部の小窓に説明分を表示させるようにしました。アイテムを使う処理は未実装のため、現状ではあくまでアイテムが一覧として確認できるだけになります。
アイテムの購入
リュックの準備が整ったので、次はアイテムの購入処理に移りましょう。前回入力画面については作成したので、サービス側の処理とデータの受け渡し部分を作り込んでいきます。
リクエストのグローバル化
今までPOSTデータはコントローラーのトレイトとしてサニタイズ・ポストデータの取得処理を組み込んでいましたが、こちらもコントローラー・サービス問わずにどこからでも呼び出せるよう、この機会にグローバル化させます。
リクエストクラス(/Classes/Request.php)
<?php
// リクエスト(送信データの格納オブジェクト)
class Request
{
/**
* @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;
}
/**
* 送信データの取得(ドット記法対応)
* @param dot_key:string
* @return mixed
*/
public function request($dot_key)
{
$keys = explode('.', $dot_key);
$values = $this->post;
foreach($keys ?? [] as $key){
$values = $values[$key] ?? '';
}
return $values;
}
}
こちらもドット記法で多次元配列を取得できるように、requestの処理内でexplodeからのforeachで要素を取り出す仕様にしました。
requestのグローバル化は以下のとおりです。
リクエストのグローバル関数(/App/Globals/RequestGlobal.php)
<?php
// クラス読み込み
require_once($root_path.'/Classes/Request.php');
$global_request = new Request;
/**
* 送信された値の取得
* @param dot_key:string
* @return mixed
*/
function request($dot_key='')
{
global $global_request;
return $global_request->request($dot_key);
}
これでサービスへの引数を経由させることなく呼び出せるようになりました。
お金の計算と商品の提供
それでは、商品の購入・おこづかいの計算処理を、作成したグローバルリクエストを用いながらサービス化していきましょう。
フレンドリィショップ用サービス(/App/Services/Home/ShopService.php)
<?php
$root_path = __DIR__.'/../../..';
// 親クラス
require_once($root_path.'/App/Services/Service.php');
/**
* フレンドリィショップ
*/
class ShopService extends Service
{
/**
* @var object::Player
*/
protected $player;
/**
* @return void
*/
public function __construct($player)
{
$this->player = $player;
}
/**
* @return void
*/
public function execute()
{
switch (request('do')) {
// 購入
case 'buy':
$this->buy();
break;
// 売却
case 'sell':
$this->sell();
break;
}
}
/**
* 購入
* @return void
*/
private function buy(): void
{
$class = config('shop.'.request('order'));
if(empty($class)){
setMessage('指定されたアイテムは販売しておりません');
return;
}
// アイテムをインスタンス化
$item = new $class;
// 購入金額の算出
$price = $item->getBidPrice() * request('count');
if($this->player->getMoney() < $price){
setMessage('おこづかいが足りません');
return;
}
// 残金調整とアイテムの獲得処理
$result = $this->player
->addItem($item, request('count'));
if($result){
$this->player
->subMoney($price);
setMessage('毎度ありがとうございました');
}else{
setMessage('お客さん、そんなに持てませんよ');
}
}
/**
* 売却
* @return void
*/
private function sell(): void
{
//
}
}
売却機能は一旦後回しにして、購入機能のみを実装しました。
販売していないアイテムを購入できないよう、注文番号で値を受け取り、config内と照合しています。また、フロント側でも制御していますが、お小遣いを超過していれば購入できないようにバックグラウンド側でも制限をかけています。
アイテムの追加処理は、プレイヤークラスにメソッドとして持たせています。
アイテムの追加(/App/Traits/Class/Player/ClassPlayerItemTrait.php)
/**
* アイテムの追加
* @param item:object::Item
* @param count:integer
* @return boolean
*/
public function addItem(object $item, $count=1): bool
{
if(is_null($item->getMax())){
// 個数計算しないアイテムの場合はnullをセット
$count = null;
}else{
// 数チェック
if($count < 1){
$count = 1; # 最小値
}
if($item->getMax() < $count){
$count = $item->getMax(); # 最大値
}
}
// アイテムのクラスを取得
$class = get_class($item);
// 現在所有しているかどうかの確認
$key = array_search(
$class,
array_column($this->items, 'class')
);
if($key === false){
// 所有していない
$this->items[] = [
'class' => $class,
'count' => $count
];
return true;
}else{
// 個数計算するアイテム且つ最大量を超過しなければ個数を加算
if(
!is_null($count) &&
($this->items[$key]['count'] + $count) <= $item->getMax()
){
$this->items[$key]['count'] += $count;
}else{
return false;
}
return true;
}
}
現在未実装ですが、自転車などのアイテムは複数個所有することができません。なので、そういったアイテムも考慮しつつ、追加処理を作成しました。
既に所有している場合はcountを追加、持っていなければ新しく配列として追加するようにしています。
では実際にアイテム購入の流れを見てみましょう。
アイテムを購入することができましたね。これでフレンドリィショップの購入処理は完成です。
本番環境への反映
PHPポケモン第76回時点での最新版を本番環境へ反映させました。エラー回避のため、セーブデータのリセットを実行しています。
前回からの追加要素は以下の通りです。
- プレイヤー機能の実装
- 野生ポケモンの追加(ニャース・メタモン)
- フレンドリィショップの追加(購入のみ)
- アイテム機能の追加(使用不可)
その他細かな要素も追加されていますので、是非遊んでみてください。
最低限の動作チェックは行なっておりますが、α版のため不具合が発生する場合があります。もしお気づきの点がありましたら、お問い合わせいただけると幸いです。