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);
SJISCSV を開いています
何が問題か 判りますか?
$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);
Made with Slides.com