844
828

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPAdvent Calendar 2015

Day 22

PHPを使いもせずDISってる君達へ

Last updated at Posted at 2015-12-21

PHPはよくDISられることがあります。しかし、実際にはほとんどPHPを利用していない人が印象だけでDISってることが多いような気がします。

そこで、PHPがよくDISられている点について、実際どうなのかをPHP未体験者向けに解説していきたいと思います。PHPを触ったことない人でもわかりやすいようにシンプル目な仕様のRubyを例に説明していきたいと思います!( Ruby触ったことなくても、その他のOOP言語を触ったことあれば雰囲気は理解できるように書いています )

DIS例1 / PHPは配列操作がしづらい

PHPの配列操作は扱いづらい等とDISる人たちがいます。実際のところどうでしょうか。
以下のような処理を配列への中間変数を用いず行うコードを例に考えてみます。

0. [2,4,6,8,10]という配列を用意して
1. ↑の配列から8以下の数だけを選択した配列を作る
2. ↑の配列から各要素を2乗した配列を作る
3. ↑の配列から20以上の数だけを選択した配列を作る
4. ↑の配列の値を掛け合わせる

上の処理をPHPとRuby、それぞれのコードを比較してみましょう。まずはRubyから。

#rubyの場合
p [2,4,6,8,10]
  .select{|num| num <= 8; } # 1. ↑の配列から8以下の数だけを選択した配列を作る
  .map{|num| num**2 }       # 2. ↑の配列から各要素を2乗した配列を作る
  .select{|num| num >= 20 } # 3. ↑の配列から20以上の数だけを選択した配列を作る
  .reduce(:*)               # 4. ↑の配列の値を掛け合わせる

↑特に違和感はないコードですね。他のOOP言語でも似たような感じになると思います。(例えばJavaならStream、C#ならLINQを使えば大体同じようなコードになるでしょう)

では次に同様の処理のPHPコードを見てみましょう。

<?php 
# PHPで上のRubyコードと同様の処理を書いた場合
print array_reduce( # 4. 配列の値を掛け合わせる
    array_filter( # 3. 20以上の数だけを選択した配列を作る
        array_map( # 2. 各要素を2乗した配列を作る
            function( $num ) {
                return $num * $num;
            },
            array_filter( # 1. 8以下の数だけを選択した配列を作る
                [2,4,6,8,10],
                function($num){
                    return $num <= 8;
                }
            )
        ),
        function( $num ){
            return $num >= 20;
        }
    ),
    function($curry,$num){
        return $curry*$num;
    },
    1
);

いかがでしょうか! PHPの場合でも、きちんと書くことができました。整然さや可読性はRuby(や他言語)のほうが上ですが、その分インデント数やタイプ量はPHPのほうが多いので好みが分かれるかもしれません。
とはいえPHPを触ったことない人には読みづらいと感じるかもしれませんので、少し解説をしておきます。

PHP Tips 1 : PHPの配列操作は引数の順番が重要

PHPでは配列を処理する関数はarray_***といったメソッドで行うのですが、これらは引数の何番目に配列を渡すかは関数ごとに違います

例えば上の例でもあるようにarray_filterarray_filter($array,func)のように1番目の引数に配列をわたしますが、array_maparray_map(func,$array)のように2番目の引数に配列を渡します。慣れてないと最初は整合性のなさから順番に混乱するかもしれませんが、しっかり覚えれば問題ありません。

PHP Tips 2 : PHPは配列操作以外でも引数の順番が大事

上の話は配列に限らず、文字列、数値などPHPのあらゆる関数にもいえることです。
例えばPHPの文字列操作を見てみると以下のように 文字列関数ごとに対象文字列を何番目の引数に渡すかが違います。

<?php

// str_replace(置換)は対象文字列を「1番目」の引数に渡す
$replaced = str_replace("one two three", "two", "TWO");  // "one TWO three";

// explode(文字列分割)は対象文字列を「2番目」の引数に渡す
print explode(" ", $replaced);  //["one", "TWO", "three"]

他言語と比べてみると、例えばRubyのように文字列がObjectで表現される言語であれば以下のように操作できます。

# rubyの例 : 文字列がObjectなので,そもそも対象文字列を引数に渡す必要はない
#   ( gsubで置換、splitで分割 )
p "one two three".gsub("two","TWO").split(" ") # ["one", "TWO", "three"]

rubyなどのOOP言語は文字列を何番目の引数に渡すか考える必要がなかったり、chainで書けたりする点はPHPよりも使いやすいなどと考える方もいるかもしれませんが、慣れて順番をしっかり覚えれば問題ありません。

PHP Tips 3 : PHPのin_arrayの面白い挙動 - 暗黙的型変換を使いこなそう

他にPHP配列の注意点としてin_arrayに触れておきます。各言語には配列が特定の値を持っているかチェックする関数が用意されていることが多いとおもいます。例えばRubyなどの場合、配列に特定の値が含まれているかのチェックは以下のようにします。

rubyのArray#includeの場合
# 2という数値が含まれているか
[0,1,2,3].include?(2);  # true

# "hoge"という文字列が含まれているか?
[0,1,2,3].include?("hoge"); #false

PHPではin_arrayという関数が用意されています。in_arrayは引数の 2番目 に対象の配列を渡します。

phpのin_array関数
<?php

# 2という数値が含まれているか
in_array(2,[0,1,2,3]);  // true

# "hoge"という文字列が含まれているか?
in_array("hoge",[0,1,2,3]);  // true
// trueになってしまった!

あれ? [0,1,2,3]"hoge"が含まれている ことになってしまいました。
PHPを使ったことのない方には違和感のある結果かもしれませんが。これはPHPにおいては
仕様通りの挙動であり、自然な動きです。

なぜかというとPHPのin_arrayはデフォルトでは暗黙的に型をcastして比較を行います。
"hoge"という文字列型は数値型にcastされると0になります。そして配列には0が含まれているので結果はture。なので[0,1,2,3]"hoge"が含まれているといえます。

なお型情報も含め、厳格に比較したい場合には第3引数にtrueを入れることで厳格な比較を行います。

<?php

// 呪い(まじない)として第三引数にtrueを入れる
in_array("hoge", [0,1,2,3], true ); // false

型付きで比較されるようになりましたね。このようにPHPでは"hoge" == 0が成立することから、比較が内部で行われそうな関数を利用する場合などは、注意を払って使う必要があります。
一見問題に見えますが、どの関数をどのように使えば型付きで比較してくれるかを暗記していれば何の問題もありません。

PHP Tips 4: PHPでは開いたものを閉じたほうが良いとは限らない

そういえば、PHPを使ったこと無い方は今まで出たサンプルコードの1行目に<?phpという謎の文字列があることに違和感を感じたかもしれません。これは何でしょうか?

PHPの仕様として<?php /* code */ ?>のようにコードを囲む必要があります。かつPHPは<?php /* code */ ?>以外の部分は出力するという仕様です。そのため、?>で閉じたはいいけれど、その後にファイルの末尾などに改行があると、改行がプログラムの出力となってしまいます。

そこでPHPではクラスファイルやスクリプトファイルでは<?phpは開くけど閉じないというのがマナーとなっています。
他の言語を利用の方々は開くのに、対応する閉じる部分を書かないことに気持ち悪さや違和感を感じる方も多いかもしれませんが、これはPHPではごく自然なことです。

整合性を気にしすぎず、一種の呪い(まじない)だと捉えましょう

DIS例2 / PHPは配列型と辞書(HaspMap)型が区別不能な言語!

多くの言語では配列型(Array)と辞書型(Dictionary,HashMap)型が分かれていますがPHPでは連想配列と呼ばれる両者が混ざったような概念しかありません。( PHP7より内部的には純粋な配列が誕生しましたが、外側からのインタフェースは従来通りです)

このことでDISられることも多いですが、本当に問題でしょうか?
以下のような点だけ気をつけて使えば問題ありません。

PHP Tips 5 : array_filterが歯抜けになるのに気をつけよう

歯抜けとはどういうことでしょうか?
例えば以下のようなケースを考えてみましょう。

1. [1,2,3,4]という配列から奇数だけをフィルタリング
2. その配列からindexが1 (=2番目)の要素を取り出す
# rubyであれば以下のように書く
[1,2,3,4,5,6]
  .select{|i| i.odd? } # 1. 奇数だけをフィルタリング
  .at(1)               # 2. indexが1(=2番目)の要素を取り出す
  # 3( 2番目の奇数 )

2番目の奇数である3が出力されましたね。同じようにPHPで書いてみましょう。

<?php // 開きの呪い(まじない)、閉じてはならない

// 1.奇数だけをフィルタリング
$filtered = array_filter([1,2,3,4,5,6],function($i){ return $i % 2 !== 0; });

// 2. indexが1の要素を取り出すと?
echo $filtered[1];
// NULL

2番目の奇数を取ろうとすると結果はNULLでした! 不思議に思う方もいるかもしれませんが、これはPHPの仕様であり、正しい挙動です。$filteredの中身を見てみましょう。

<?php // indexがfilter前から変わっていない!
print_r($filtered);
// Array
// (
//     [0] => 1
//     [2] => 3
//     [4] => 5
// )

ご覧の通り、indexがfilter前から変わってません。なので3の値はindex:1ではなくindex:2(当初の配列で3番目だったため)にあります。
なぜか?これはPHPの配列がRubyでいうHashだと考えれば理解できると思います。

つまりRubyで同じようなコードを表現すると以下のようになります。

# phpの[1,2,3,4,5,6]はrubyで表現すると以下のようになる
hash = { "0"=> 1,  "1"=> 2, "2"=> 3,  "3"=> 4, "4" => 5, "5" => 6 }

# hashに対して奇数だけにフィルタリング. "index"情報は保持される
filtered = hash.select{|k,v| v.odd? }  # {"0"=>1, "2"=>3, "4"=>5}

# 「"1" => 2」は偶数なので消えてる
p filtered["1"] # nil

PHPの連想配列はArrayのように使えるけれども、Hashとして動くためこのような挙動になります。
そのためPHPの連想配列を、いわゆるArrayのように使いたければarray_filterを読んだ後array_valuesという関数を呼ぶと良いです。(RubyのHash#valuesのようなもの)

つまりRubyの[1,2,3,4,5,6].select{|i| i.odd? }と同じような処理をPHP
で書くには以下のように書けばOKです

<?php 

$filtered = array_values( // 歯抜けを防ぐ呪い(まじない)
    array_filter([1,2,3,4,5,6],
        function($i){ return $i % 2 !== 0; }
    )
);

// 3( 2番目の奇数 )が取り出せる
echo $filtered[1];

配列に対して、array_filterを使った後はarray_valuesをする。この呪い(まじない)さえ覚えておけば、PHPのarray_filterも怖くはありませんね。

PHP Tips 6: array_mergeに気をつけよう!

辞書(HashMap)型データのmergeはよくやることだと思います。PHPでも当然、それを行うための関数array_mergeがあります。使い方を見てみましょう。

<?php

// keyに記号, valueに読み方を保持した2種類の連想配列をmerge
$a = [ "_" => "underbar" ];
$b = [ "*" => "asterisk",  "@" => "atmark" ];
$c = array_merge($a,$b);

// $cには2つの配列がmergeされた結果が入っている
//  $c : (
//      [_] => underbar
//      [*] => asterisk
//      [@] => atmark
//  )

意図通りに動きましたね。では違うデータでもう一度試してみましょう!
次は記号の読み方ではなく、数字の読み方を入れてみましょう。

<?php // keyに数字の文字列, valueに読み方を保持した2種類の連想配列をmerge
$a = [ "10" => "ten" ];
$b = [ "20" => "twenty",  "30" => "thirty" ];
$c = array_merge($a,$b);

// !!あれ? なぜか$cは [ "ten", "twenty", "thirty" ]になってしまった.
// $c : Array
// (
//     [0] => ten
//     [1] => twenty
//     [2] => thirty
// )

!なぜか array_mergeをかけるとindex(添え字)が連番になってしまいkeyとvalueの関係情報が失われてしまいました

これはarray_margeの仕様通りの挙動です。仕様を見てみましょう。

http://php.net/manual/ja/function.array-merge.php
入力配列の中にある数値添字要素の添字の数値は、 結果の配列ではゼロから
始まる連続した数値に置き換えられます。

つまり keyのindexに数値(型が文字列であっても)の場合は、辞書型ではなく配列型の値をmergeしたのと同じ結果 になります。

代わりに+を使ってmergeすると意図通りの挙動になります

<?php // keyに数字の文字列, valueに読み方を保持した2種類の連想配列をmerge
$a = [ "10" => "ten" ];
$b = [ "20" => "twenty",  "30" => "thirty" ];
$c = $a + $b;

// $c : Array
// (
//     [10] => ten
//     [20] => twenty
//     [30] => thirty
// )

上を見て辞書同士の結合にはarray_mergeの代わりに+を使えば安心と思うかもしれません。しかしarray_merge+は右辺、左辺のどちらかを優先するかが異なるので注意してください。

<?php // +とarray_mergeの挙動の違い

$default = [ "size" => 10 ];
$input   = [ "size" => 3  ];

$merged1 = array_merge($default,$input);
// [size] => 3

$merged2 = $default + $input;
// [size] => 10

このように他言語と違ってPHPの配列の結合は考える点は多いですが、特殊な挙動を1つ1つ覚えて注意しながら利用することで、他言語と同じように利用できるので、特に何の問題もないといえるでしょう。

PHP Tips 7 : JSONの空配列、空Objectに要注意!

さて、今まで話したようにPHPには配列型と辞書型は曖昧なものです。
しかし当然ながら、一般的な言語やデータフォーマットはそうではありません。
このことはPHPから他フォーマットにデータを変換する際、例えば連想配列をJSONに変換したりする際に問題になる可能性があります。

JSONは配列型はArrayと辞書型はObjectで表現されます。
例えばJSONでは空の配列は[]、空のObjectは{}のように表現します。
では配列でもあり辞書でもあるPHPの[]はどちらに変換されるでしょうか?やってみましょう。

<?php
$json = json_encode([]);  // "[]"  空のArrayなJSON

空のArrayになりました。では空の"{}"のように空Objectに変換したい場合はどうすれば良いのでしょうか?以下のようにstdClassを用いることで表現できます。

<?php
$json = json_encode (new stdClass);   // "{}"を表現する呪い(まじない)

実際のケースはもう少し複雑になるでしょう。例えばJSON変換前にarray_filterなどをしていて、それが空になる可能性がある場合は、その連想配列はJSONのArray型として扱いたいのか、Object型として扱いたいのかで対応を変える必要があります。

またindex(添え字)は数値だけれども、辞書型(JSONのObject型)として利用している場合にも、ケアしましょう。

DIS例3 / 予測しづらい副作用が多すぎる

PHPには確かに、いくつか予期せぬ副作用が起きたり、破壊的な挙動をしたりという挙動がいくつかあります。しかし無限にあるわけではないので、それらの挙動を暗記しておいて気をつければなんの問題もありません。

いくつか注意が必要な挙動をあげておきます。

PHP Tips 8 : Global変数を知っていれば意外な副作用も怖くない!

PHPでは他言語より広い範囲でGlobal変数が利用されることがあります。例えばfile_get_contentsという関数で外部ページのデータを取得するケースを考えてみます。

<?php

// 引数にURLを渡せばページ内容を
$content = file_get_contents("http://qiita.com/");
echo $content
//  <!DOCTYPE html><html xmlns:og="http://ogp.me/ns#">....

一見$html以外のHTTP Header等のメタ情報は取れないようなインタフェースに見えます。
さて、ここでおもむろに$http_response_headerという変数の中身を見てみましょう。

<?php

$content = file_get_contents("http://example.com/");
print_r( $http_response_header );
// 全然関係なさそうに見える変数$http_response_headerの中身を覗いてみると...
// Array
// (
//    [0] => HTTP/1.0 200 OK
//    ...
// )

HTTPのメタ情報が入っています!HTTP/1.0 200 OKを頑張って文字列パースすることで200というステータスコードを手にいれることもできます。
そう、file_get_contentsのヘッダー情報はグローバル変数$http_response_headerに格納されるのです。

これは、逆にいうとうっかり$http_response_headerという変数名を使っていると意図せずに値が書き換わってしまう可能性があることを意味しています。
これは一見、問題に見えるかもしれませんが$http_response_headerがグローバル変数ということを知っていて利用しないように注意すれば何の問題もありません。

一般的なOOP言語では副作用を閉じ込めるためにオブジェクトを利用しますが、PHPの標準関数ではグローバル領域や配列への参照に副作用を与えることが多々あります。そう聞くと不安になるかもしれませんが、グローバル変数をきちんと暗記しておけば変なバグが入ることもありません。

またparse_strというQueryをパースする関数にも要注意です。

<?php
// parse_strはURLのクエリ部分をパースする関数だが返り値はない...
// どうやって取り出す?
parse_str("site_name=qiita&user_id=nori0620");

// 宣言した覚えのない変数達が!
echo $site_name;  // "qiita"
echo $user_id;  // "nori0620"

上のように デフォルトの挙動では、そのスコープに勝手に変数を作ってしまいます。
第二変数に配列を渡すと、その配列に値を入れてくれるようになります。第二引数を忘れないようにしましょう。

PHP Tips 9 : 破壊的なメソッドに注意しよう!

PHPでは他言語ではimmutableな実装が一般的なオブジェクトでも、カジュアルにmutableな実装になっていたり、他言語では非破壊的なインタフェースが用意されている関数が破壊的なインタフェースしか用意されてないことがないことも多いです。しかし、どのメソッドが破壊的かきちんと覚えておけば問題ありません。いくつか紹介しておきましょう。

例えばDateTimeオブジェクトなどは他言語ではValueObjectとしてimmutableな実装になっていることが多いですが、PHPの場合はmutableな実装になっています。
例を見てみましょう。

<?php
// 記事インスタンス:$articleが投稿日:postedTimeをDateTimeで持っているとする
$article->postedTime = new DateTime('2016-12-20');
echo "記事の投稿日: " . $article->postedTime->format('Y年m月d日');
   // => '記事の投稿日: 2016年12月20日'

// 投稿日の2日後を表示
$twoDays    = new DateInterval('P2D');;
$twoDaysAfterPost = $postedTime->add( $twoDays );
echo "投稿の2日後 :" .$twoDaysAfterPost->format('Y-m-d'); 
   // => '投稿の2日後: 2016年12月22日'

// 再度、投稿日を表示してみると...投稿日が進んでしまっている!
echo "記事の投稿日: " . $article->postedTime->format('Y年m月d日');
   // => '記事の投稿日: 2016年12月22日'

上のコードはDateTime#addが破壊的であることを意識せずに書いてしまっているため、$article->postedTimeの値が壊れてしまっています。

DateTimeには非破壊的に変更した値を返すメソッドはありません。 そのためObjectをcloneしてから操作するように気をつけましょう。

<?php
// 以下のようにcloneしてから値を変更するようにしよう!
$twoDaysAfterPost = clone $article->postedTime;
$twoDaysAfterPost = $twoDaysAfterPost->add( $twoDays );

またDateTimeImmutableというImmutableな実装クラスがあるので、もし自分で利用する場合は、そちらを利用するのも良いでしょう。しかし多くのライブラリやフレームワークはDateTimeのほうを利用しているので、上記の注意はPHPを触っていく上で必要になるでしょう。

またPHPのarrayに関しても非破壊的なアクセスしか用意されてないメソッドが多く、例えば多くのsort系関数は破壊的です。sort前の値も必要な場合はコピーをとってからsortするように気をつけましょう。

つまり

いかがでしょうか?PHPは多少、整合性などに問題はあるかもしれませんが 「配列関数、文字列関数、数値関数などのそれぞれの挙動がバラバラな引数の順番を覚える」「暗黙的型変換に気をつける」「内部で暗黙的変換が行われる関数にも気をつける」「<?phpで開いても、閉じないように気をつける」「array_filterをかけた際は歯抜けになっていることに気をつける」「配列のindexが数値かどうかで挙動が変わることに気をつける」「配列をJSONに変換したときにArray or Objectな挙動に気をつける」「PHPの連想配列を使うときは配列型と辞書型に区別がないことを意識して使う」「グローバル変数を覚えておいて衝突しないように気をつける」「メソッドによっては、デフォルトの挙動がそのスコープ内に勝手に変数を作るので気をつける」「破壊的なメソッドを覚えておき、挙動に気をつける」 などなどの点を、しっかり注意しながら使いさえすれば他言語と遜色なく扱えることが理解いただけたのではないでしょうか。

呪い(まじない)めいたルールを覚えたり、危険な副作用を意識したくない人たちへ

上で説明したようにPHPの標準関数は整合性がなかったり、予測しづらい副作用が多いです。これを、「人間が覚えたり、気をつけたり、精神論で頑張る」という方針でカバーするのは難しいと感じる人達もいるかもしれません。

もう少しスマートな方針としては 標準関数を利用せず、それをラップした外部ライブラリを利用 するのが良いでしょう。PHPの生みの親,ラスマス・ラードフ氏もインタビューで以下のように答えています。

http://gihyo.jp/news/report/2015/12/1401?page=4
整合性をとるのはフレームワークの役割です。
フレームワークは問題を解決するための全体的なアプローチを定める場所ですから。
PHPはその下のレベルにあって,ただ低レベルにあるライブラリとか
関数とかへのアクセスを提供するだけの存在です。

これは PHPで標準関数は低レベルなレイヤーな実装であり、アプリケーションコードのようなレイヤーで利用するようなものではない と、とらえることもできます。例としていくつかPHPの標準関数をラップした例をあげてみましょう。

連想配列をラップしたCollectionクラス

PHPは配列自体に振る舞いを追加することはできませんが ArrayAccess , IteratorAggregate, Countable などのInterfaceをimplementするとObjectを配列のように振る舞わせることはできます ( ArrayLikeなオブジェクトを作ることができる )

これを利用して最近のPHPフレームワークは、各フレームワークごとにCollectionクラスを独自に実装しています。( Collection関係のメソッドは互換性はなく、ArrayLikeな点のみにおいて互換するという感じの温度感ですね。 )

このように各フレームワークごとにarray_****関数を直接使うことの問題を回避するために、各々のCollectionライブラリを作っています。

<?php // LaravelのCollectionの利用例. 
  // array_mapやarray_filterのように引数順を気にせずよく、chainしてかける
$collection = collect(['taylor', 'abigail', null])
    ->map(function ($name) { return strtoupper($name); })
    ->reject(function ($name) { return empty($name); });


HTTP Requestをラップしたクラス

$_GET / $_POST などのグローバル変数、session_***、headerなどのWEBの入出力に関する標準関数をラップしたライブラリとして有名なものにSymfonyで利用されているHttpFoundation があります。これはSymfonyで利用されてますが、独立したコンポーネントとして利用することもできます。

またPSRというPHPの標準規約でこれらをラップする際のインタフェース - PSR7が策定されました。全体的にImmutableな設計になっており、グローバルな$_GETをを上書きしてしまってバグが出るなどということは起きづらいでしょう。(これはあくまでインタフェースなので標準関数をどのようにラップするかは各々の実装になります。 )

他にも

他にも多くの標準関数をラップしたライブラリたちが存在します。

  • ファイルシステム関係の関数をラップしたライブラリ Symfony Filesystem Component
  • HTTPクライアントとしての処理をラップしたライブラリ Guzzle
  • URLのパース関係の標準関数をラップしたライブラリ Purl
  • 文字列関係の標準関数をラップしたライブラリ Stringy
  • などなど

まとめ

これらのライブラリを使えば、アプリケーションレベルのレイヤーで標準関数を直接使うことは少なくなるでしょう。またラップしたものが見当たらなければ自分自身でラッパーライブラリを書くのも良いでしょう。

このように、PHPの標準関数は低レイヤーのAPIであって、なるべく直接使わない、と割り切れば、整合性がおかしい問題や、意図せぬ副作用による危険性は、ある程度、解消されるのではないでしょうか。

もちろん、文字列や数値などのプリミティブをラップしたオブジェクトを扱う場合、生成コストなどが問題になる場合があるかもしれませんが、その場合は適宜、柔軟に「呪文めいたコードを書くコスト」と「リソースへのコスト」のどちらを優先するかのトレードオフで判断していけば良いでしょう。

844
828
13

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
844
828

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?