PHP基本クイズ
2015-10-03 PHPカンファレンス 2015
GMOリサーチ 寺田渉
facebook: 寺田渉
github: waterada
twitter: @wa_terada
この動き、あなたは知っていますか
自己紹介 (会社)
- PHP (CakePHP) を主に使って開発
- 継続的インテグレーション
- github + git flow で運用
- PHPUnit で カバレッジ 100%
- Behat (Selenium Driver 経由の画面テスト) 利用
- vagrant で開発環境構築
自己紹介 (趣味)
CakePHP 公式ドキュメント 翻訳
自己紹介 (趣味)
ボードゲーム 翻訳
自己紹介 (趣味)
TED 翻訳
自己紹介
プログラミング & 翻訳
大好き人間です
今日は、PHPで
ハマリそうなポイントを
クイズ形式で紹介します。
はじめに
今日はできるだけ皆さんに参加して頂き、
より良い時間にしたいと思っておりますので、
質問、
間違いの指摘、
もっとよい解法など
その場でどんどん発言お願いします!
(joind.in でも評価&コメントよろしくお願いします!)
2:00
$a = "abc";
if ($a == 0) {
echo "a is zero!"; //これが実行される!
}
なぜ?
<?php
$a = "abc";
if ($a == 0) {
echo "a is zero!"; //これが実行される!
}
int にキャストされる (非数字以降は無視される) から。
避けるためには === を使うこと。
左右どちらかが文字列でない可能性があるなら == は危険。
答え
$a = "10000000000000000.1";
$b = "10000000000000000.2";
if ($a == $b) {
//実行される!
echo "true!";
}
これも知っておこう
== では できるだけ数値に キャストしようとする!
$a = "01";
if ($a == "1") {
//実行される!
echo "true!";
}
$a = "abc";
switch ($a) {
case 0: // $a == 0
echo "0"; //これが出力される
break;
default:
echo "default";
}
switch も == で比較してる
3:30
$num = 10000000000000001;
echo number_format($num);
// 10,000,000,000,000,000
// と出力される
なぜ末尾が 0 に なった?
$num = 10000000000000001;
echo number_format($num);
// 10,000,000,000,000,000
// と出力される
答え
float にキャストされるから。
4:00
empty()
empty($var)
$var が下記の場合以外でも true になりえるが何?
・null ・値が未設定 ・(boolean) false ・(integer) 0 ・(float) 0.0
・(string) ''
・(array) []
・空のタグから作成された
SimpleXML オブジェクト
答え
$var = '0';
empty($var); //←これは true
文字列の '0' が空だと
判定されることを忘れないこと。
4:30
って、どちらも動きが
同じに 見える。
違い ってあるの?
if ($var) {
if (!empty($var)) {
と
前者は $var が 未定義 の場合に
下記の Notice が出る
if ($var) {
if (!empty($var)) {
答え
※なお、後者は下記と同義:
PHP Notice: Undefined variable: var
if (isset($var) && $var) {
5:15
何が 問題 でしょう?
<?php class MySample { public $param1; } ?>
MySample.php
答え
?> の後ろに 空白 がある
<?php class MySample { public $param1; } ?> ←ここに実は空白が存在している
?>以降に文字 があれば PHP はそれを出力する。 すると httpヘッダは出力済になる。 すると リダイレクト等はできず、代わりに真っ白の画面が表示。 しかし どのファイルが出力しているのか特定しづらい。
(PHPのみのファイルで) ?> は 書かないで!
6:15
配列の + と array_merge() はどう違う?
$a = ['a' => 'A1', 'b' => 'B', 'C'];
$b = ['a' => 'A2', 'd' => 'D', 'E'];
var_export($a + $b);
var_export(array_merge($a, $b));
答え
$a = ['a' => 'A1', 'b' => 'B', 'C'];
$b = ['a' => 'A2', 'd' => 'D', 'E'];
【$a + $b】
key : 先勝ち
index : 先勝ち
[ //$a $b 'a' => 'A1', //A1 A2 ★ 'b' => 'B', //B 0 => 'C', //C E ★ 'd' => 'D', // D ]
【array_merge($a, $b)】
key : 後勝ち index : (0から)再連番 [ //$a $b 'a' => 'A2', //A1 A2 ★ 'b' => 'B', //B 0 => 'C', //C 'd' => 'D', // D 1 => 'E', // E ★ ]
※ + は連想配列用。
混乱を避けるために array_merge で統一 するのも手。
7:45
foreach で 書き換え たら異変が!
$array1 = [1,2];
foreach ($array1 as &$val) {
$val = 0; //0で書き換え
}
$array2 = [3,4];
foreach ($array2 as $val) {
//何か
}
var_export($array1); // [0, 4] なぜ 4 ここに!?
var_export($array2); // [3, 4]
答え
$array1 = [1,2]; foreach ($array1 as &$val) { $val = 0; //0で書き換え } unset($val); //かならずこれが必要 $array2 = [3,4]; foreach ($array2 as $val) { //何か }
でも、もっとオススメの方法が・・・
書き換えなら array_walk を
$array1 = [1,2];
array_walk($array1, function(&$val) {
$val = 0; //0で書き換え
});
//これなら危険は無い。これがオススメ。
$array2 = [3,4];
foreach ($array2 as $val) {
//何か
}
9:45
$fh = fopen($path, 'r');
while (($data = fgetcsv($fh)) !== false) {
array_walk($data, function(&$val) {
$val = mb_convert_encoding($val,'UTF-8','SJIS');
});
// $data を使う処理
}
fclose($fh);
SJIS の CSV を開いています
何が問題か 判りますか?
$fh = fopen($path, 'r');
while (($data = fgetcsv($fh)) !== false) {
array_walk($data, function(&$val) {
$val = mb_convert_encoding($val,'UTF-8','SJIS');
});
// $data を使う処理
}
fclose($fh);
答え
エンコード前に fgetcsv を呼んではいけません。
SJIS や UTF-16LE の場合、全角文字の 途中に
望ましくない文字が含まれることがあるためです。
ならば、 どうすれば いい?
答え
$fh = fopen($path, 'r');
stream_filter_append($fh,
'convert.iconv.cp932/utf-8', //SJIS の場合
STREAM_FILTER_READ);
while (($data = fgetcsv($fh)) !== false) {
// $data を使う処理
}
fclose($fh);
ストリームフィルタ を使う!
'convert.iconv.utf-16le/utf-8', //UTF-16LEの場合
さらに知っておくと良い物: Stream_Filter_Mbstring
12:15
$fh = fopen("test.csv","r");
$a = fgetcsv($fh);
fclose($fh);
var_export($a);
なぜ?
"a\"",b"
array ( 0 => 'a\\"', 1 => 'b"', )
test.csv
実行
出力結果(2列になってる!)
array ( 0 => 'a\\",b', )
期待していた出力結果
↓
答え
array fgetcsv ( $handle, $length = 0,$delimiter = ",", $enclosure = '"', $escape = "\" )
『\』 ($escape) 直後の 『"』 ($enclosure) は
閉じ引用符とは見なさない という指定が
デフォルトであるため。
fgetcsv($fh, 0, ",", '"', "\0")
下記のようにほぼ存在しない文字にしておくのも手。
とはいえ、デフォルトで問題のあるケースもほとんど無いとは思うが。
13:45
「SPL」
知ってます?
Standard PHP Library (SPL)
(標準で入っているライブラリ)
便利なものがたくさんあります。
今日はその中でも特に便利な
Iterator を紹介します。
Iterator を
すごく簡単に説明すると
メモリ に優しくて
疎結合 にしやすい
foreach で回せるやつ
//data1 読み込み
$fh = fopen($data1_path, "r"); //CSV読込
fgetcsv($fh); //ラベル行Skip
while (($line = fgetcsv($fh)) !== false) {
if (empty($line[0])) { continue; } //空欄行Skip
//メインの処理
}
fclose($fh);
//data2 に対しても同等の処理を行う
foreach ($data2 as $line) {
if (empty($line[0])) { continue; } //空欄行Skip
//メインの処理(上記と同じもの)
}
Iterator 以前 のコード
$data1 = new SplFileObject($data1_path); //CSV読込
$data1 ->setFlags(SplFileObject::READ_CSV);
$data1 = new LimitIterator($data1, 1); //ラベル行Skip
$all = new AppendIterator();
$all->append($data1); //結合
$all->append(new ArrayIterator($data2));
$all = new CallbackFilterIterator($all, //空欄行Skip
function($val) { return !empty($val[0]); });
foreach ($all as $line) { //メインの処理
}
Iterator を 使った コード
16:15
class MyRangeIterator implements Iterator {
private $START;
private $END;
private $SKIP;
private $n;
public function __construct($start, $end, $skip = 1) {
list($this->START, $this->END, $this->SKIP)
= [$start, $end, $skip];
}
public function rewind() { $this->n = $this->START; }
public function next() { $this->n += $this->SKIP; }
public function valid() { return ($this->n <= $this->END); }
public function key() { return null; }
public function current() { return $this->n; }
}
foreach (new MyRangeIterator(1, 10000000, 2) as $n) {
} //全てをメモリ展開しないのでメモリに優しい
Iterator は自作できます
PHP5.5 から
もっと 簡単に Iterator が
作れるようになりましたね?
そう! ジェネレータ構文 です!
yield って書くやつですね。
function generateMyRange($start, $end, $skip = 1) {
for ($n = $start; $n <= $end; $n += $skip) {
yield $n;
}
}
foreach (generateMyRange(1, 10000000, 2) as $n) {
}
同じものを yield で書くと
簡単ですね!
17:45
正規表現
こういうやつ
if (preg_match('/[^0-9]/', $str)) {
//正の整数じゃないよエラー
}
ある文字列が定義したパターン(=正規表現)に
合致するかどうかを簡単にチェックできます。
置き換えもできます。
知らないなら すぐに
調べた方がいいです
絶対効率よい
機能すれば良いんじゃありません。
メンテしていけるかどうかが重要!
if(preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:
\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2
F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C
[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(
?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[
^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--
)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?
:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1
,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})
(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:
25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1
[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $email)) {
メアドチェックする正規表現 (http://emailregex.com/ より)
でも、こういうのは、つらい。
18:45
前置きはこのくらいにして、
何が 問題 でしょう?
if (! preg_match('/^[0-9]+$/', $str)) {
//0~9以外の文字が含まれていたら
$ は終端という意味 じゃない
$str = "123\n" でもOKとなってしまう! ($ は終端 or 終端の改行という意味)
$str = "123\n" でも意図通りエラーとなる!
(\z は終端という意味)
答え
if (! preg_match('/^[0-9]+$/', $str)) {
if (! preg_match('/^[0-9]+\z/', $str)) {
19:45
その結果は下記のとおり。 A E C が消えてしまった。どうすれば良かった?
$html = "A <b>B</b> C <b>D</b> E";
$html = preg_replace('#<b>.*</b>#', '', $html);
何が 問題 でしょう?
<b>~</b>をすべて撤去しようとしたのだが…
? (非貪欲マッチ) を指定する
A <b>B</b> C <b>D</b> E → A E
―――
―――
$html = "A <b>B</b> C <b>D</b> E";
$html = preg_replace('#<b>.*?</b>#', '', $html);
A <b>B</b> C <b>D</b> E → A C E
$html = "A <b>B</b> C <b>D</b> E";
$html = preg_replace('#<b>.*</b>#', '', $html);
答え
20:45
$str = "aa\n" . "bb\n"; $str = preg_replace('/^|(\n)(.)/', '$1- $2', $str);
- aa - bb
もっと 良い方法 は?
という具合に行頭に - を付けたいだけなのだが、
なんだか解りづらい。
行頭 という指定ができればいいのだが…
マルチライン m を指定する
$str = "aa\n" . "bb\n"; $str = preg_replace('/^|(\n)(.)/', '$1- $2', $str);
$str = "aa\n" . "bb\n";
$str = preg_replace('/^/m', '- ', $str);
答え
結果:
- aa - bb
結果:
- aa - bb
21:45
$str = "<img >"; if (preg_match('/<img.*?>/', $str)) {
何が 問題 でしょう?
<img>タグを撤去しようとしたのだが…
結果はマッチしない。
どうすべき?
. は基本改行含めない
→ 合致しない
→ 合致する
if (preg_match('/<img.*?>/', $str)) {
if (preg_match('/<img.*?>/s', $str)) {
答え
$str = "<img >";
23:15
正規表現を
メンテしやすく
書くノウハウ
デリミタ変えれるよ
/aa/bb/cc/dd → dd へと置換
$html = preg_replace('{^(/[^/]+)+/}', '', $html); $html = preg_replace('#^(/[^/]+)+/#', '', $html);
$html = preg_replace('/^(\/[^\/]+)+\//', '', $html);
23:45
(?: ) は $1 等で参照しない ( )
// 1 2 3
$pattern = '/^(?:.*?),(?:.*?),(?:.*?),(\w+)/(\w+)/(\w+)$/';
$replace = '$3-$2-$1';
$str = preg_replace($pattern, $replace, $str);
参照しないことを 明示 できる。
読みやすくなると思ったら使おう。
後方参照がたくさんあるなら
コメントあると解りやすい
// 1 2 3 4 5 6
$pattern = '/^(.*?),(.*?),(.*?),(\d+)/(\d+)/(\d+)$/';
$replace = '$6-$5-$4 $2:$3:$1';
$str = preg_replace($pattern, $replace, $str);
24:45
x 使って わかりやすく書く
if (preg_match(
"/^-?(?:[1-9]\d*|0)(?:\.\d+[1-9])?\z/",
$num)) {
数値表現が妥当かのチェック
if (preg_match("/
^ # 先頭
-? # マイナスは有っても無くても良い
(?:
[1-9] # 整数部一桁目は0禁止
\d* # 整数部
|
0 # 値 0 のみ整数部の1桁目が 0 でも良い
)
(?:
\. # 小数点
\d* # 小数部
[1-9] # 小数部の末尾は 0 禁止
)? # 小数部は無くてもいい
\z # 末尾
/x", $num)) {
25:30
マジック メソッド
って知ってますか?
__get() や __call() 等
特殊な動きをするメソッド
__get() が呼ばれるのは何番?
class A {
public $p1 = 1;
protected $p2 = 2;
private $p3 = 3;
}
class B extends A {
public $p4 = 4;
protected $p5 = 5;
private $p6 = 6;
public function __get($name) { return "G"; }
}
$b = new B();
echo $b->p1 . $b->p2 . $b->p3; //親のプロパティ
echo $b->p4 . $b->p5 . $b->p6; //自身のプロパティ
echo $b->p7; //存在しないプロパティ
( __get() : プロパティがない場合に代わりに呼ばれる )
答え
class A { public $p1 = 1; //1 protected $p2 = 2; //__get() private $p3 = 3; //__get() } class B extends A { public $p4 = 4; //4 protected $p5 = 5; //__get() private $p6 = 6; //__get() public function __get($name) { return "G"; } }
$b = new B();
echo $b->p1 . $b->p2 . $b->p3;
echo $b->p4 . $b->p5 . $b->p6;
echo $b->p7;
27:45
つまり、
アクセスできない
ものが __get() になる!
1. == の不思議 2. number_format() 3. empty() 4. if() と if(!empty()) 5. 末尾の ?> 6. + と array_merge() 7. foreach の 怪現象 8. ストリームフィルタ 9. fgetcsv() の $escape 10. イテレータ有り無し
11. イテレータ 自作, yield 12. 正規表現とは 13. 正規表現 $ 14. 正規表現 ? 15. 正規表現 m 16. 正規表現 s 17. 正規表現 デリミタ変更 18. 正規表現 ( ) (?: ) 19. 正規表現 x 20. マジックメソッド
感想などあれば:
joind.in: https://joind.in/15328 facebook: 寺田渉 github: waterada twitter: @wa_terada
以上です! ご質問があれば!
ご静聴ありがとうございました!
class MyRangeIterator implements Iterator {
private $START;
private $END;
private $SKIP;
private $n;
public function __construct($start, $end, $skip = 1) {
list($this->START, $this->END, $this->SKIP)
= [$start, $end, $skip];
}
public function rewind() { $this->n = $this->START; }
public function next() { $this->n += $this->SKIP; }
public function valid() { return ($this->n <= $this->END); }
public function key() { return null; }
public function current() { return $this->n; }
}
foreach (new MyRangeIterator(1, 10000000, 2) as $n) {
} //全てをメモリ展開しないのでメモリに優しい
yield 使わないと
時間が余ったら→
$input に '\E' が入っていると困るから。 preg_quote() を使おう。
if (preg_match('/\Q'.$input.'\E/', $memo)) {
if (preg_match('/'.preg_quote($input,'/').'/', $memo))
\Q と \E を ユーザ入力に使っちゃダメ
言明
(?= (?! (?<= (?<!
マッチするけど結果に 含めないとかいうやつ
バグの温床になるので注意!
メンバー全員テストケース
出せるくらいの共通理解が無いと
\Q と \E で見やすく
$str = preg_replace('/\(\*\^-\^\*\)/', '', $str);
$str = preg_replace('/\Q(*^-^*)\E/', '', $str);
\Q : 引用(Quotation) 開始 \E : 引用 終了(End)
この範囲は正規表現の文字とはみなされなくなる。
ついでに array_merge_recursive()
$a = ['a' => ['b' => 1]];
$b = ['a' => ['c' => 2]];
$c = array_merge_recursive($a, $b);
var_export($c);
下記ではどうなる?
$a = ['a' => 1];
$b = ['a' => 1];
$c = array_merge_recursive($a, $b);
var_export($c);
[
'a' => [
'b' => 1,
'c' => 2,
]
]
⇛
⇛
?
答え
array_merge_recursive は再帰的にマージするものだが、
マージする対象に配列以外があった場合は、
配列に変換 してマージする。
$a = ['a' => 1];
$b = ['a' => 1];
$c = array_merge_recursive($a, $b);
var_export($c);
[
'a' => [
0 => 1,
1 => 1,
]
]
⇛
配列を まとめて代入したい
list($id, $name) = $array;
$id = $array[0]; $name = $array[1]; これは list() 使えば下記のように書けますね!
ネストもできる
$array = [
1,
[2, 3],
];
list($a, list($b, $c)) = $array;
PHP 5.5 からは foreach でも使える
$array = [
[1, 2],
[3, 4],
];
foreach ($array as list($a, $b)) {
注意!
$a = [];
list($a[0], $a[1]) = ["a", "b"];
echo implode(",", $a);
PHP 5 では (右から順に代入されて) b,a と出力 $a: [1 => "b", 0 => "a"]
PHP 7 では
(左から順に代入されて) a,b と出力
list() で 添字代入すると混乱する
$a = ['a', 'b'];
$b = ['c', 'd', 'e'];
【$a + $b】 array ( 0 => 'a', 1 => 'b', 2 => 'e', )
【array_merge($a, $b)】
array(
0 => 'a',
1 => 'b',
2 => 'c',
3 => 'd',
4 => 'e',
)
ただの配列だったら?
連番を作る
// 0 ~ 100 までの偶数値の配列を作る
var $even = [];
for ($i = 0; $i <= 100; $i += 2) {
$even[] = $i;
}
もっと簡単な方法は無いのだろうか?
答え
$even = range(0, 100, 2);
PHP基本クイズ!! この動き、あなたは知っていますか
By Wataru Terada
PHP基本クイズ!! この動き、あなたは知っていますか
実際に現場でハマったPHPの不思議な挙動とその原因・対応策をクイズ形式でご紹介します。また、意外と知らない人の多かったPHPの便利な機能についてもご紹介します。
- 16,565