PHPのforeachの罠
foreachの$valueは参照渡しではない
$array
に格納されている要素を、すべて空文字列に置き換えたいとします。
方法はいろいろありますが、今回はforeach
の話なので、foreach
を使います。
<?php $array = array("php", "python", "ruby", "javascript"); foreach ($array as $value) { $value = ""; } var_dump($array);
実行結果は次のようになります。
array(4) { [0]=> string(3) "php" [1]=> string(6) "python" [2]=> string(4) "ruby" [3]=> string(10) "javascript" }
要素はそのままになっていますね。これは、$value
を直接変更したからです。
PHPの場合、foreach
で取り出した要素は、その要素をコピーする形になります。
$value
は、$array
の要素と同じアドレスを見ていない、というわけです。
解決策1
上記では、値渡しになっているので、参照渡しにすれば良い、という考え方です。
<?php $array = array("php", "python", "ruby", "javascript"); foreach ($array as &$value) { $value = ""; } unset($value);
$value
の前に&
をつけて参照渡しにしています。
こうすることで、すべての要素を空文字列にすることができました。
注意点として、処理後、unset($value)
で参照渡しをクリアしてやることが大事です。
処理したあとも、参照渡しである状態が保持されつづけると、思わぬバグの温床となります。
解決策2
参照渡しにするのではなく、$array
の方を直接書き換える、という手法になります。
<?php $array = array("php", "python", "ruby", "javascript"); foreach ($array as $key => $value) { $array[$key] = ""; }
キーを取得して、そのキーで要素を指定することによって配列を操作しています。
僕はこちらの方をよく使っています。
PDOのfetch系メソッド
前提
- PHP 5.6
- mysql Ver 14.14 Distrib 5.7.18
- Google Chromeでしかテストしていません
- PDOのfetch系メソッドをまとめただけです
テストデータを用意
※ fetch系メソッドだけ調べたい方は下の方まで飛ばしてください
ターミナルでMySQLを起動。それから以下の命令を実行します。
create database fetch_test default charset utf8;
もちろんphpMyAdmin
からでもOKです。
show databases;
で作成できたか確認できます。databases
と複数形であることに注意しましょう。
確認ができたら、use fetch_test
でDBを選択します。
文房具を管理するitems
テーブルを作成します。
create table items ( id int auto_increment not null primary key, name varchar(30), price int(30) );
show tables
で確認できます。 こちらもDBを表示した時と同じくtables
と複数形であることに注意です。
ここでようやくテストデータの挿入です。
適当にinsertしていきます。
insert into items (name, price) values ('鉛筆', 100); insert into items (name, price) values ('ノート', 120); insert into items (name, price) values ('消しゴム', 50);
終わったらselect * from items;
でチェックしておきましょう。
id | name | price |
---|---|---|
1 | 鉛筆 | 100 |
2 | ノート | 120 |
3 | 消しゴム | 50 |
メソッドを切り替えてfetch系メソッドを検証するクラスをつくる
同じディレクトリ下に
という3つのファイルを作成します。
database.php
<?php class Database { public static function connectDB() { $pdo = ''; try { $pdo = new PDO( // 環境によってはhost部分をlocalhostにしてください 'mysql:dbname=fetch_test;host=127.0.0.1;charset=utf8', 'root', '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ] ); } catch (PDOException $e) { header('Content-Type: text/plain; charset=UTF-8', true, 500); exit($e->getMessage()); } return $pdo; } }
静的メソッドでデータベースに接続するだけのクラスです。本来はDB名、ユーザ名、パスワードなど
は別ファイルに定数として分割するものですが、ここでは省略します。
PDO::ATTR_DEFAULT_FETCH_MODE
はPDOのfetch系メソッドで引数が省略された場合などのフェッチスタイルを決定します。
ここではPDO::FETCH_ASSOC
で設定しています。これは、カラム名をキーとする連想配列で取得するモードになります。
また、コメントアウトにもあるとおり、環境によってはhostの部分を127.0.0.1
からlocalhost
にしてください。僕の環境では逆で
ないと動きませんでした。
fetchTester.php
<?php require_once 'database.php'; class FetchTester { private $pdo; private $table = 'items'; function __construct() { $this->pdo = Database::connectDB(); } public function fetch() { $stmt = $this->selectTable(); while ($row = $stmt->fetch()) { var_dump($row); } } public function fetchObject() { $stmt = $this->selectTable(); while ($row = $stmt->fetchObject()) { var_dump($row); } } public function fetchColumn($fetchColumnNumber) { $stmt = $this->selectTable(); while (false !== $val = $stmt->fetchColumn($fetchColumnNumber)) { var_dump($val); } } public function fetchAll() { $stmt = $this->selectTable(); $rows = $stmt->fetchAll(); var_dump($rows); } private function selectTable() { $sql = 'select * from ' . $this->table; $stmt = $this->pdo->query($sql); return $stmt; } }
fetch系メソッドをそれぞれクラス固有のメソッドに分けています。唯一、前処理に当たるselectTable
のみprivate
で定義し、
残りのアクセス権はindex.php
で使えるようにpublic
にしてあります。
index.php
<?php require_once 'FetchTester.php'; $fetch_tester = new FetchTester(); // この部分のメソッドを変更して使う $fetch_tester->fetchAll();
実際に実行するメソッドを実装するファイルです。動作を確かめる際、このファイルへアクセスします。
ここではfetchAll
になっていますが、動作を確認したいfetch系メソッド名に切り替えることで、その動きをvar_dump
形式で確認することができます。var_dump
が嫌だ、という方はecho
なりprint
なりに書きかえてもらって構いません。値をreturn
する形式にして汎用性を高めても良いです。
1. fetch
では、順番に見ていきましょう。
まず基本形のfetch
メソッドです。
$fetch_tester->fetch();
出力結果は以下のようになります。
array(3) { ["id"]=> string(1) "1" ["name"]=> string(6) "鉛筆" ["price"]=> string(3) "100" } array(3) { ["id"]=> string(1) "2" ["name"]=> string(9) "ノート" ["price"]=> string(3) "120" } array(3) { ["id"]=> string(1) "3" ["name"]=> string(12) "消しゴム" ["price"]=> string(2) "50" }
fetch
は取得し終えるとfalseを返すので、それを条件分岐として利用することができます。
fetchTester.php
では以下の部分です。
while ($row = $stmt->fetch()) { // 処理 }
2. fetchObjcet
連想配列ではなく、オブジェクトとしてデータを取得します。
$fetch_tester->fetchObject();
結果は以下のような感じ。
object(stdClass)#4 (3) { ["id"]=> string(1) "1" ["name"]=> string(6) "鉛筆" ["price"]=> string(3) "100" } object(stdClass)#5 (3) { ["id"]=> string(1) "2" ["name"]=> string(9) "ノート" ["price"]=> string(3) "120" } object(stdClass)#4 (3) { ["id"]=> string(1) "3" ["name"]=> string(12) "消しゴム" ["price"]=> string(2) "50" }
3. fetchColumn
引数で指定した番号のカラムを文字列で取得します。省略時は0を指定したとみなされます。
$fetch_tester->fetchColumn();
今回使用しているfetchTester.php
では、fetchTesterクラスのfetchColumnメソッドの引数から、PDO
のfetchColumnメソッドの引数へとカラム番号を受け渡す形になるよう実装してあります。
while
内で、false !==
としているのは値に0が入る場合を考慮してです。参考にさせていただいたサイトのソースコードが
しっかりしていると思ったので、そのまま採用させていただきました。
public function fetchColumn($fetchColumnNumber = 0) { $stmt = $this->selectTable(); while (false !== $val = $stmt->fetchColumn($fetchColumnNumber)) { var_dump($val); } }
以下は、引数に2を指定した場合の結果。
string(3) "100" string(3) "120" string(2) "50"
4. fetchAll
2次元配列として全データを取得します。
$fetch_tester->fetchAll();
ブラウザには以下のように出力されます。
array(3) { [0]=> array(3) { ["id"]=> string(1) "1" ["name"]=> string(6) "鉛筆" ["price"]=> string(3) "100" } [1]=> array(3) { ["id"]=> string(1) "2" ["name"]=> string(9) "ノート" ["price"]=> string(3) "120" } [2]=> array(3) { ["id"]=> string(1) "3" ["name"]=> string(12) "消しゴム" ["price"]=> string(2) "50" } }
参考
ComposerでSmartyを入れて遊ぶだけ
Composer
最近のPHPerには必須のツール。PHP系ライブラリをインストールする際、依存関係を標準的に管理してくれる。(ある ライブラリをインストールした際、あのライブラリも要るのかあ、という状況を解決してくれる)
Smarty
PHPのテンプレートエンジン。nodeでいうEJSなどである。
ComposerをMacにインストール
環境はMac OS Sierraでやっていきます。
まず、Composerのインストーラをcurl
で取得、その後インストーラをPHPで実行します。
curl -sS https://getcomposer.org/installer | php
次にcomposer.phar
を移動します。
mv composer.phar /usr/local/bin/composer
入れたComposerのバージョンを確認するにはcomposer -V
で調べます。
Smartyを入れる
なんか適当にディレクトリをつくって、その中に潜りましょう。
mkdir smarty_app cd smarty_app
先にcomposer.json
を書いてインストールしても良いですが、面倒なのでcomposer requrie
というコマンドを
使います。このコマンドを使った場合、composer.json
は勝手に作成されます。
composer require smarty/smarty
現時点でのsmarty_app
の中の構成はこのような感じになっているはずです。
composer.json composer.lock vendor
vendor
の中を見ると、ちゃんとsmartyが入っています。
必要最小限の構築
次にtemplates
というディレクトリと、templates_c
というディレクトリを作成します。
そして、このtemplates_c
の権限を変えます。
mkdir templates mkdir templates_c chmod 777 templates_c
このtemplates_c
というディレクトリは必須です。ここではあんまり良くないですが、777で許可しています。
イメージとしては、templates
というディレクトリに入っているテンプレートが、自動的にコンパイルされて
templates_c
に入ります。(cはコンパイルのcみたいですね)
使ってみる
index.php
をsmarty_app
直下につくります。
<?php require_once 'vendor/autoload.php'; ini_set('date.timezone', 'Asia/Tokyo'); define('MY_TITLE', 'TEST'); $smarty = new Smarty(); // 使うテンプレートが入っているディレクトリを指定 $smarty->setTemplateDir('./templates/'); $smarty->assign('hello', 'Hello, Smarty!!'); $smarty->assign('today', new DateTime()); $smarty->assign('animal', array('rabbit','cat','dog')); $smarty->display('index.tpl');
つづいて、templates
内にhader.tpl
、index.tpl
を作成。
header.tpl
<!DOCTYPE html> <meta charset="utf-8"> <title> {$page_title} </title>
index.tpl
{* コメントアウト *} {include file='header.tpl' page_title={$smarty.const.MY_TITLE}} {* 普通の変数 *} <p>{$hello} {* メソッド *} <p>{$today->format('Y/m/d (D)')} {* 予約変数 *} <dl> <dt>現在のタイムスタンプ <dd>{$smarty.now} <dt>現在処理中のテンプレートファイル名 <dd>{$smarty.template} <dt>Smarty version <dd>{$smarty.version} <dl> {* 配列を逆順でループ(step=-1だから) *} <ul> {section name=i loop=$animal step=-1} <li>{$animal[i]} {/section} </ul>
一応smarty_app
内の構造を記しておきます。
|--composer.json |--composer.lock |--index.php |--templates | |--header.tpl | |--index.tpl |--templates_c |--vendor
つくったら、PHPのビルトインサーバを立てるなりして、http://localhost/~
なんかでアクセスします。
成功していればブラウザ上に、Smartyのバージョンや現在の日付なんかが表示されるはずです。
templates_c
内を見ると、コンパイルされたファイルがあるのもわかるはずです。