Help us understand the problem. What is going on with this article?

PHPオブジェクト指向入門(前半)

More than 5 years have passed since last update.

コンセプト

オブジェクト指向プログラミング未経験者~理解を深めたい人、ノンケ~ホモまで幅広くカバーするつもり。多分。

クラスとオブジェクト(初級)

唐突ですが、量産型のロボットの設計・製造について考えてみましょう。

ロボ太郎 ロボ次郎
ロボ太郎 ロボ次郎
イラストで初心者を釣る

クラス

まず、ロボットの設計図を クラス として定義します。設計図をもとにロボットを製造するには、 new 演算子を使います。製造された物体のことを オブジェクトインスタンス と呼びます。ここではこれらの用語を区別せずに用いることにします。

Yahoo!知恵袋 - オブジェクトとインスタンスの違い

Robotクラス
class Robot { }
実行コード
$a = new Robot;
$b = new Robot;

ここまでは直感的に理解できますね。

プロパティ

最初の画像では

  • ロボ太郎
  • ロボ次郎

と勝手に呼び分けていましたが、実際は

  • 変数 $a に代入されたロボット
  • 変数 $b に代入されたロボット

の違いしかありませんでした。名前のないロボットも可哀そうなので、名前を記録するための プロパティ を確保してやりましょう。

Robotクラス
class Robot {
    public $name;
}
実行コード
$a = new Robot;
$a->name = 'ロボ太郎';
$b = new Robot;
$b->name = 'ロボ次郎';

echo $a->name; // ロボ太郎;
echo $b->name; // ロボ次郎;

さて、何やらごちゃごちゃしてきました。焦らずちょっとずつ見ていきましょう。

public って何?

アクセス権を表現するためのキーワードです。 public はどこからでもアクセス可能なことを意味します。プロパティの定義には、アクセス権の明示が必要です。

-> って何?

アロー演算子 と呼ばれます。インスタンスのプロパティを参照する演算子です。

何で $a->name なの? $a->$name じゃないの?

…PHPという言語ではそう決められているからです、としか言えませんね。

ちなみに $a->$name は、プロパティの名前に対して $name に代入されている値を指定するという意味です。以下の2つは等価です。

echo $a->name;
$property = 'name';
echo $a->$property;

代入する前にはどうなってるの?

NULL が設定されています。インスタンス生成時に設定される値を NULL 以外にしたければ、初期値の設定が必要です。

初期値を空文字列に設定する
class Robot {
    public $name = '';
}

以降はこの記述を使用します。

カプセル化

製品というものは、やっぱり外装がしっかりしている方が安心ですよね。パソコンでマザーボードをさらけ出したまま使っている人はほとんどいないでしょう。外部からの衝撃で簡単に壊れてしまいそうですしね。メーカーにもそういう使い方をした場合は「サポート対象外」であると告げられることだろうと思います。

プログラミングにおいて、外部からのアクセスを禁止し、内部に隠蔽してしまうことを カプセル化 といいます。(厳密な定義とは少し異なりますが、この段階ではそういう説明にしておきます)

このロボットの例で例えましょう。PHPの変数はデータの型を気にせずどんなものでも好き勝手代入することが出来ますが、もし $name配列 が代入されちゃったら何か嫌な感じですね。それをあたかも文字列であるかのように見なして表示しようとしたら Notice: Array to string conversion 等のエラーが発生しちゃいますし。

それでは、勝手に名前を魔改造されるのが嫌なのでアクセス権を private にしてみましょう! private は外部からのアクセスを全て遮断します。

Robotクラス
class Robot {
    private $name = '';
}
実行コード
$a = new Robot;
$a->name = 'ロボ太郎'; // エラー
$b = new Robot;
$b->name = 'ロボ次郎'; // エラー

echo $a->name; // エラー
echo $b->name; // エラー

正常な代入も弾いちゃったら意味が無いじゃん!こんなの機械じゃない!…はい、機械には操作パネルが必要ですね。

メソッド

それではこのロボットの内部に、外部から入力したい名前を受け取って文字列であれば設定する メソッド を設けてみましょう。メソッド自体は外部からアクセス可能でなければ意味が無いので public に設定します。メソッドに対するアクセスは、プロパティ同様に -> を使って行います。

Robotクラス
class Robot {
    private $name = '';
    public function setName($name) {
        $this->name = (string)filter_var($name);
    }
}

メソッドってもうちょっと分かりやすく言うとつまり何?

クラスに所属する関数のようなものです。

$this って何?

インスタンス自身を指す特別な変数です。例えば、

$a->setName('ロボ太郎');

のようにコールされたとき、setNameメソッド内で $this$a を表していることになります。

代入するとき何してんのこれ?

全てを文字列にエラーなく変換する方法です。以下のリンクを参照してください。

Qiita - $_GET, $_POSTなどを受け取る際の処理

Qiita - PHPで各種バリデーション

さて、これで実行してみましょう。

実行コード
$a = new Robot;
$a->setName('ロボ太郎');
$b = new Robot;
$b->setName('ロボ次郎');

echo $a->name; // エラー
echo $b->name; // エラー

まだ表示するときにエラーが出ますね。当然です、 private に設定したからには 書き込み だけでなく 読み取り 用のメソッドも作成する必要があります。

Getter/Setterパターン

Robotクラス
class Robot {
    private $name = '';
    public function setName($name) {
        $this->name = (string)filter_var($name);
    }
    public function getName() {
        return $this->name;
    }
}
実行コード
$a = new Robot;
$a->setName('ロボ太郎');
$b = new Robot;
$b->setName('ロボ次郎');

echo $a->getName(); // ロボ太郎
echo $b->getName(); // ロボ次郎

やったぜ。

オブジェクト指向の基本中の基本を習得することが出来ました。おめでとう。ちなみにこれは Getter/Setterパターン と呼ばれ、オブジェクト指向の根幹を為す技法です。

オブジェクト指向の特長

  • 外部には機能的にまとめられた最低限必要な情報しか公開しない。
  • ライブラリ開発や複数人での開発に向いている。
  • 大規模になってきてもメンテナンスがしやすい。

クラスとオブジェクト(中級)

__construct メソッド (コンストラクタ) の利用

__construct という名前で コンストラクタ メソッドを作ると、インスタンス生成時に自動的にコールされます。このような特別なメソッドを総称して マジックメソッド と呼びます。ここでは、わざわざ後からsetNameメソッドをコールしなくても良いようにしてみましょう。

Robotクラス
class Robot {
    private $name = '';
    public function __construct($name) {
        $this->setName($name);
    }
    public function setName($name) {
        $this->name = (string)filter_var($name);
    }
    public function getName() {
        return $this->name;
    }
}
実行コード
$a = new Robot('ロボ太郎');
$b = new Robot('ロボ次郎');

echo $a->getName(); // ロボ太郎
echo $b->getName(); // ロボ次郎

さっきまで new Robot って書いてたけど、 new Robot() じゃなくて良かったの?

PHPでは問題ないようです。Javaではダメです。

マジックメソッドって他に何かあるの?

PHP Manual - マジックメソッド

上記に全部記載されているので興味があればどうぞ。クラスを扱う上でほぼ必須となってくるのはコンストラクタぐらいで、他のメソッドを実装するかどうかはプログラマに委ねられます。

基本クラス stdClass

stdClass は以下のようにデフォルトで定義されています。

class stdClass { }

PHPでは未定義のインスタンスプロパティを新たに作成することが出来ます。連想配列と似た使い方ですね。言うまでも無く、この操作で自動的に作成されるプロパティは public です。

$a = new stdClass;
$a->name = 'ロボ太郎';
echo $a->name; // ロボ太郎;

また、配列と stdClass は相互にキャストが可能です。

stdClassから配列へ
$array = (array)$stdClass;
配列からstdClassへ
$stdClass = (object)$array;

但し、 数字添え字配列 に対して (object) キャストは使用しないでください。アクセス不能なプロパティが発生してしまいます。あくまで 連想配列 だけを対象としてください。この事象に関しては 暇人向けの考察 で考察します。

オブジェクトと参照

PHP Manual - リファレンスの説明
PHP Manual - オブジェクトと参照

詳しいことは上記にまとめられているので、ここでは更に要点を絞ります。

通常の値渡し

$b の値に $a の値がコピーされます。

$a = 'value';
$b = $a;

通常の値渡し

通常のリファレンス渡し

$a$b が常に同じ値を参照するようになります。

$a = 'value';
$b = &$a;

通常のリファレンス渡し

オブジェクトIDの値渡し

$b の値に $a の値がコピーされます。

オブジェクトは実際にはIDという によって管理されています。これは、 var_dump 関数で表示したときに #1 のような数字で得られます。オブジェクトIDが一致するとき、それらは全く同一のオブジェクトであることを意味します。

$a = new stdClass;
$b = $a;

オブジェクトIDの値渡し

オブジェクトIDのリファレンス渡し

$a$b が常に同じ値を参照するようになります。

オブジェクトIDのリファレンス渡しをする必要性が生まれることは稀です。

オブジェクトIDのリファレンス渡し

$a = new stdClass;
$b = &$a;

静的プロパティ / 静的メソッド / クラス定数

ここまでは全て インスタンスプロパティインスタンスメソッド を取り扱ってきました。ロボットの個体それぞれが持つプロパティとメソッドですね。ところが、設計図そのものが持つ 静的プロパティ / 静的メソッド / オブジェクト定数 も存在します。オブジェクト定数は静的プロパティと同様に静的に定義される値なので本来は クラス定数 と呼ぶべきですが、何故かこの名前がついています。

class Sample {
    public static $property; // 静的プロパティ
    public static function method() { } // 静的メソッド
    const OBJECT_CONSTANT = null; // オブジェクト定数
}
  • :: のことを スコープ定義演算子 と呼びます。
  • オブジェクト定数は全て public 相当です。
  • オブジェクト定数には必ず初期値の設定が必要です。
  • オブジェクト定数に対して配列・オブジェクト・リソースを代入することは出来ません。
  • オブジェクト定数に対して再代入を行うことは出来ません。
  • クラス内にて自らのインスタンスを生成する場合、new クラス名 の代わりに new self が使えます。

厳密な対応表

  • オブジェクト定数については、静的プロパティとほとんど扱いが同じなので省略します。
  • 複数可能性があるものについては、以下の表中で最も一般的なものを太字で表しました。

…しかし、こんな設計ミスを含んでいるようなごちゃごちゃしたものは覚えなくていいです。

インスタンス
プロパティ
インスタンス
メソッド
静的プロパティ 静的メソッド
外部から
-> 演算子で
$v->name $v->name() × $v->name()
外部から
:: 演算子で
×
(非推奨警告)
クラス名::$name
$v::$name
クラス名::name()
$v::name()
インスタンス
メソッド内から
-> 演算子で
$this->name $this->name() ×
(不可能なのに
バグで非推奨警告)
$this->name()
インスタンス
メソッド内から
:: 演算子で
× $this::name() self::$name
クラス名::$name
$this::$name
self::name()
クラス名::name()
$this::name()
静的
メソッド内から
-> 演算子で
× × × ×
静的
メソッド内から
:: 演算子で
× × self::$name
クラス名::$name
self::name()
クラス名::name()

一般的な書き方に限定した対応表

覚えるならこちらで!

インスタンス
プロパティ
インスタンス
メソッド
静的プロパティ 静的メソッド
外部から $v->name $v->name() クラス名::$name クラス名::name()
インスタンス
メソッド内から
$this->name $this->name() self::$name self::name()
静的
メソッド内から
× × self::$name self::name()

実践例(1) - htmlspecialchars, html_entity_decode のラッパークラス

単なる静的メソッドの集合体としてクラスを定義することも可能です。この場合、インスタンスを作成できないように private なコンストラクタ を定義しておくべきでしょう。

クラス定義
class Html {
    public static function encode($str) {
        return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
    }
    public static function decode($str) {
        return html_entity_decode($str, ENT_QUOTES, 'UTF-8');
    }
    private function __construct() { }
}
使用例
$html = '<strong>Test</strong>';
$html = Html::encode($html);
echo $html; // &lt;strong&gt;Test&lt;/strong&gt;
$html = Html::decode($html);
echo $html; // <strong>Test</strong>

実践例(2) - Factory Method パターン の実装

ロボットに「色」という情報を追加してみましょう。但し、以下の条件を満たすものとします。

  • (ロボットの外装は製造工程で決まるので)一度色を決めたら二度と変更できないようにする。
  • (工場の生産ラインは限られているので)色を redblue に限定する。色の指定が間違っていた場合は red を適用する。
Robotクラス
class Robot {
    private $name = '';
    private $color;
    public function __construct($name, $color) {
        $this->setName($name);
        $this->color = $color === 'blue' ? 'blue' : 'red';
    }
    public function setName($name) {
        $this->name = (string)filter_var($name);
    }
    public function getName() {
        return $this->name;
    }    
    public function getColor() {
        return $this->color;
    }    
}
実行コード
$a = new Robot('ロボ太郎');
$b = new Robot('ロボ次郎', 'blue');
echo $a->getColor(); // red
echo $b->getColor(); // blue

一見、これで条件を満たせているような気がします。…しかし、PHPのコンストラクタはこんな呼び出し方も出来てしまうのです。

実行コード
$a = new Robot('ロボ太郎', 'red');
echo $a->getColor(); // red
$a->__construct('ロボ太郎', 'blue');
echo $a->getColor(); // blue

おっと、色が変わってしまいました!何か手を打つ必要がありそうです。ここで、 Factory Method パターン を導入します。

  • コンストラクタの第2引数に red を渡して生成したインスタンスを返す createRedRobot メソッドを静的に作成します。
  • コンストラクタの第2引数に blue を渡して生成したインスタンスを返す createBlueRobot メソッドを静的に作成します。
  • コンストラクタを private にし、外部から直接インスタンスを生成出来ないようにします。

こうすればコンストラクタの第2引数には redblue しか渡されないことが保証されたので、

$this->color = $color === 'blue' ? 'blue' : 'red';

なんて面倒なことを書かなくても

$this->color = $color;

で十分ですね!一石二鳥です。

Robotクラス
class Robot {
    private $name = '';
    private $color;
    public static function createRedRobot($name) {
        return new self($name, 'red');
    }
    public static function createBlueRobot($name) {
        return new self($name, 'blue');
    }
    private function __construct($name, $color) {
        $this->setName($name);
        $this->color = $color;
    }
    public function setName($name) {
        $this->name = (string)filter_var($name);
    }
    public function getName() {
        return $this->name;
    }  
    public function getColor() {
        return $this->color;
    }    
}
実行コード
$a = Robot::createRedRobot('ロボ太郎');
echo $a->getColor(); // red
$a->__construct('ロボ太郎', 'blue'); // エラー
echo $a->getColor(); // red

これで色が不変であるということも保証できました。

エラー

PHPは古くから エラー を出力して異常時の対応を行ってきました。例えば、 htmlspecialchars 関数は第1引数に 配列 を受け取った時、以下のようなエラーを発します。返り値はマニュアルに記載されている通り false となります。

ソースコード
var_dump(htmlspecialchars(array()));
実行結果
Warning:  htmlspecialchars() expects parameter 1 to be string, array given in ... on line ...
bool(false)

自分で定義する関数やメソッド内でも trigger_error 関数によりエラーを発生させることが出来ます。試しに、除算を行う関数 div を作成してみましょう。ただ、ゼロ除算に普通に着目するだけでは面白くないので…

  • $\frac{n}{0}(n \gt 0)$ は $\infty$ とする
  • $\frac{n}{0}(n \lt 0)$ は $-\infty$ とする
  • $\frac{0}{0}$ は false としてエラーも発生させる
  • エラーが1回でも発生したら強制終了する(戒め)

という実装にしてみましょう。 $\infty$ は定数 INF によって定義されているのでこれを利用します。

野獣

関数定義
function div($a, $b) {
    if ($a > 0 && $b == 0) {
        return INF;
    }
    if ($a < 0 && $b == 0) {
        return -INF;
    }
    if ($a == 0 && $b == 0) {
        trigger_error('ゼロ除算とか無理だってはっきりわかんだね', E_USER_WARNING);
        return false;
    }
    return $a / $b;
}
実行コード
echo "計算を始めます\n";
printf("div(1, 0) = %s\n", div(1, 0));
printf("div(-1, 0) = %s\n", div(-1, 0));
printf("div(0, 0) = %s\n", div(0, 0));
echo "計算は正常に終了しました\n";
実行結果
計算を始めます
div(1, 0) = INF
div(-1, 0) = -INF
Warning:  ゼロ除算とか無理だってはっきりわかんだね in ... on line ...
div(0, 0) = 
計算は正常に終了しました

全然正常に終了してないですね。ちゃんと条件分岐を実装しましょうか。

実行コード
echo "計算を始めます\n";
$answer = div(1, 0);
if ($answer === false) {
    die("そんなことはしてはいけない(戒め)\n");
}
printf("div(1, 0) = %s\n", $answer);
$answer = div(-1, 0);
if ($answer === false) {
    die("そんなことはしてはいけない(戒め)\n");
}
printf("div(-1, 0) = %s\n", $answer);
$answer = div(0, 0);
if ($answer === false) {
    die("そんなことはしてはいけない(戒め)\n");
}
printf("div(0, 0) = %s\n", $answer);
echo "計算は正常に終了しました\n";
実行結果
計算を始めます
div(1, 0) = INF
div(-1, 0) = -INF
Warning:  ゼロ除算とか無理だってはっきりわかんだね in ... on line ...
そんなことはしてはいけない(戒め)

一応期待通りに動作するようになりましたが、なんだかとっても記述が冗長になってしまいました。それに、PHPのエラーメッセージもそのまま表示されちゃっててとても不恰好です。これに対して…

  • 毎回エラーをチェックするように書かなくていい
  • エラーメッセージを変数にセットして好きなように扱える

こんな便利な解決策を提供してくれるモデルが、次に紹介する「例外」なのです。

例外処理

説明ばかりでは面白くないので、具体例から入りましょう。例外を用いて先ほどのコードを再構成してみます。

関数定義
function div($a, $b) {
    if ($a > 0 && $b == 0) {
        return INF;
    }
    if ($a < 0 && $b == 0) {
        return -INF;
    }
    if ($a == 0 && $b == 0) {
        throw new Exception('ゼロ除算とか無理だってはっきりわかんだね');
    }
    return $a / $b;
}
実行コード
try {
    echo "計算を始めます\n";
    printf("div(1, 0) = %s\n", div(1, 0));
    printf("div(-1, 0) = %s\n", div(-1, 0));
    printf("div(0, 0) = %s\n", div(0, 0));
    echo "計算は正常に終了しました\n";
} catch (Exception $e) {
    echo $e->getMessage() . "\n";
    echo "そんなことはしてはいけない(戒め)\n";
}
実行結果
計算を始めます
div(1, 0) = INF
div(-1, 0) = -INF
ゼロ除算とか無理だってはっきりわかんだね
そんなことはしてはいけない(戒め)

メチャクチャすっきり書けましたね。では、例外について詳しく見ていきましょう。

Exception クラス

PHPでいうところの例外は全て Exception クラス(もしくはその派生クラス)のインスタンスです。このクラスをメッセージに直結するものに絞って簡略化すると、以下のような構成になっています。デフォルトのコードは 0 です。また、Getter/Setterパターンがここにも現れていますね。

Exceptionクラスの概略
class Exception {
    private $message;
    private $code;
    public function __construct($message = '', $code = 0) {
        $this->message = $message;
        $this->code    = $code;
    }
    public function getMessage() {
        return $this->message;
    }
    public function getCode() {
        return $this->code;
    }
}

このクラスの最大の特徴、それは

throw new Exception('ゼロ除算とか無理だってはっきりわかんだね');

投げる(throw) ことが出来るということです。 「投げるって何ぞや?」 と思われた方でも簡単に理解できるようにこれを表現するなら…そうですね、脱出するための指令を送るという喩えではどうでしょう。ポケモンの 「あなぬけのヒモ」 あたりが分かりやすいんじゃないでしょうか。

あなぬけのヒモ

例外を投げるだけではそこで強制終了してしまうのと同じです。今度はこれを 捕まえる(catch) 必要があります。「あなぬけのヒモ」であればちょうどそのダンジョンの出口で捕まりますね。

例外処理の流れ
try {

    /* 1. この中で投げられた例外は */

} catch (Exception $e) { /* 2. ここで $e に代入されて */

    /* 3. ここの実行に強制的に移動する */

}

「ちょっと待った!さっき確かprintf関数呼び出し中に終わっちゃったけどその処理ってどうなったのさ?」

printf("div(0, 0) = %s\n", div(0, 0));

こういう疑問が沸いてくる人は鋭いですね。例外処理においては、それまで実行途中であった完結していない処理は無かったことにされます。非常に重要な概念ですので頭に入れておいてください。

Exception クラスは全ての例外の基底となるクラスですが、これを 継承 したクラスがいくつか存在します。継承については後ほど触れますので、ここでは 亜種のようなもの として把握しておいてください。 Exception クラスを直接扱うことはせず、例外の種類を詳しく表現している継承クラスを用いることが多いです。

LogicException クラス

プログラムの論理的な(Logic)誤りを指摘する例外です。いわば、コードを書いた自分のための警告です。この例外を捕まえる必要は基本的に無く、逆に発生しないようにプログラムを書かなければなりません

throw new LogicException('あのさぁ…(棒読み)');

RuntimeException クラス

プログラムの実行時における(Runtime)例外です。ユーザー入力・確率といった不確定要素の決定次第で発生してしまう、いわば発生しても仕方ない、発生することを想定して捕まえなければならない例外を表します。

throw new RuntimeException('ねー、今日練習きつかったねー。大会近いからね。しょうがないね(迫真)');

こ↑こ↓から更に例外が細分化されていきますが、とりあえずこの2種類を覚えておけば問題はないでしょう。興味のある人は以下のまとめを参照してください。

Qiita - PHP標準例外まとめ

また別の例として、最初のロボットの例で使っていた setName メソッドの挙動を変更してみます。以前は 「文字列にキャストさせてみて false として失敗していたらそれも強引に "" に変換する」 という処理を書いていました。

public function setName($name) {
    $this->name = (string)filter_var($name);
}

では今回はここを 「文字列にキャスト不可能な型が渡されたら例外をスローする」 という形に変更してみましょう。 LogicException を更に継承した InvalidArgumentException を使用してみます。

public function setName($name) {
    $name = filter_var($name);
    if ($name === false) {
        throw new InvalidArgumentException('ホモ特有の使い方はNG');
    }
    $this->name = $name;
}

これを用いて以下のようなコードを実行すると例外が発生し、スクリプトの実行が停止されます。

停止←setName←例外発生
$robot = Robot::createRedRobot('ロボ太郎');
$rotot->setName(array('そうだよ(便乗)'));
停止←createRedRobot←__construct←setName←例外発生
$robot = Robot::createRedRobot(array('そうだよ(便乗)'));

組み込みクラスの例外

ここまでは、あらかじめ自分で作成しておいた関数やメソッドで例外をスローするようにコーディングすることを説明しました。しかし、PHP側で自動的にスローされる例外もあります。ここではその一例を紹介します。

データベース関連

内容的には 「PHPでデータベースに接続するときのまとめ」 で紹介しているものを含んでいます。

PDOException mysqli_sql_exception
継承元 RuntimeException RuntimeException
デフォルト設定のまま
スローするメソッド
PDO::__construct のみ 無し
例外スロ―に関しての
設定を行う手段
PDO::__construct
PDO::setAttribute
mysqli_report

PDO において全てのメソッドで例外をスローするように設定する

$options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);
$pdo = new PDO($dsn, $username, $password, $options);
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

mysqli において全ての関数/メソッドで例外をスローするように設定する

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
実行可能だがパフォーマンスを考慮していない糞SQLも防ぎたい場合はこうする
mysqli_report(MYSQLI_REPORT_ALL);

SplFileObject クラス

PHPには古典的な fopen を初めとするファイル操作関数がありますが、これらをオブジェクト指向的に扱えるようにラップした SplFileObject というクラスも存在します。このクラスのメソッドの一部は false を返す代わりに RuntimeException をスローします。

クロージャ

クロージャの基本

以下のような関数があるとします。

MUR

function soudayo($option) {
    return "そうだよ({$option})";
}
echo soudayo('便乗'); // そうだよ(便乗)

PHPでは関数をオブジェクトとして生成することが出来ます。これは一般的に クロージャ / 無名関数 / ラムダ などと呼ばれます。最初の例をクロージャを利用するように書き換えてみましょう。

実行コード
$closure = function ($option) {
    return "そうだよ({$option})";
};
var_dump($closure);
echo $closure('便乗');
実行結果
object(Closure)#1 (1) {
  ["parameter"]=>
  array(1) {
    ["$option"]=>
    string(10) "<required>"
  }
}
そうだよ(便乗)

"parameter""$option" といった項目はPHPが内部的に保持しているだけの情報なので、プログラマ側が気にする必要はありません。以下に注目すべきポイントを挙げます。

  1. $closure = オブジェクト; という代入式の一部なので末尾に ; が必要である。
  2. function の後にすぐ引数の列挙が来る。関数名が存在しない。
  3. クラス名は Closure となっているが、 new Closure ではインスタンスを生成することが出来ない。あくまでこの構文を使用する必要がある。

use キーワードの利用

クロージャ特有の機能として、 use キーワードを使用してそのスコープに存在する変数をインポート出来るというものがあります。通常の関数の場合は global キーワードを使用してグローバル変数にアクセスする方法しかありませんでした。

通常の関数
$var = 'GLOBAL';
function a() {
    $var = 'A';
    return b();
}
function b() {
    global $var;
    return $var;
}
echo a(); // GLOBAL
クロージャ
$var = 'GLOBAL';
$a = function a() {
    $var = 'A';
    $b = function () use ($var) {
        return $var;
    };
    return $b();
};
echo $a(); // A

クロージャ内で再帰的に自分自身をコールしたい場合、リファレンスでuseする必要があります。クロージャが生成される瞬間にはまだ代入が済んでいないためです。以下にフィボナッチ数列の第$n$項を求めるクロージャの例を示します。

$fib = function ($n) use (&$fib) {
    switch ($n) {
        case 1:  return 0;
        case 2:  return 1;
        default: return $fib($n - 2) + $fib($n - 1);
    }
};
echo $fib(20); // 4181

コールバック型(callable)

PHPには コールバック という特殊な型が存在します。とはいっても、 var_dump したときなどに型情報として表示されるわけではありません。変数に格納した値を元に関数やメソッドの名前を動的に指定して実行するための表現形式であり、実体は以下の何れかに該当します。

クロージャ

実は先ほど紹介したクロージャの基本的な使い方は、単なるコールバックの一種に過ぎません。

$closure = function soudayo($option) {
    return "そうだよ({$option})";
};
echo $closure('便乗'); // そうだよ(便乗)

"関数名"

function soudayo($option) {
    return "そうだよ({$option})";
}
$name = 'soudayo';
echo $name('便乗'); // そうだよ(便乗)

"クラス指定::メソッド名"

主に静的メソッドのコールに用いられます。

この記述は誤りを含んでいます。
class MUR {
    public static function soudayo($option) {
        return "そうだよ({$option})";
    }
}
$name = 'MUR::soudayo';
echo $name('便乗'); // そうだよ(便乗)
この記述は誤りを含んでいます。
class MUR {
    private static function soudayo($option) {
        return "そうだよ({$option})";
    }
    public static function test() {
        $name = 'self::soudayo';
        echo $name('便乗'); // そうだよ(便乗)
    }
}
MUR::test();
重要な訂正

この記法は後に紹介する call_user_func 関数を利用したものでしか動作しないようです、皆さんは間違わないように注意してください。更に、PHP5.3以降でしか有効でないという制約もあります。

array("クラス指定", "メソッド名")

主に静的メソッドのコールに用いられます。

class MUR {
    public static function soudayo($option) {
        return "そうだよ({$option})";
    }
}
$name = array('MUR', 'soudayo');
echo $name('便乗'); // そうだよ(便乗)
__CLASS__はそれが記述されたクラス名を表すマジック定数
class MUR {
    private static function soudayo($option) {
        return "そうだよ({$option})";
    }
    public static function test() {
        $name = array(__CLASS__, 'soudayo');
        echo $name('便乗'); // そうだよ(便乗)
    }
}
MUR::test();

array(インスタンス, "メソッド名")

主にインスタンスメソッドのコールに用いられます。

class MUR {
    public function soudayo($option) {
        return "そうだよ({$option})";
    }
}
$mur = new MUR;
$name = array($mur, 'soudayo');
echo $name('便乗'); // そうだよ(便乗)
class MUR {
    private function soudayo($option) {
        return "そうだよ({$option})";
    }
    public function test() {
        $name = array($this, 'soudayo');
        echo $name('便乗'); // そうだよ(便乗)
    }
}
$mur = new MUR;
$mur->test();

call_user_func 関数の利用

コールバック型は変数に入れた状態でないと有効ではありません。JavaScriptではよく用いられる以下のような記法も残念ながらPHPでは動作しません。

BAD
echo function ($option) {
    return "そうだよ({$option})";
}('便乗'); // 構文エラー

変数に代入せずに即座にコールバックとして使いたい場合には call_user_func 関数を利用します。第1引数にコールバックを渡し、第2引数以降に本来の第1引数以降を並べていきます。

GOOD
echo call_user_func(
    function ($option) {
        return "そうだよ({$option})";
    },
    '便乗'
); // そうだよ(便乗)

またこの関数の特徴として、本来リファレンス渡しである引数に対してエラーを出さずに値渡し出来るというものがあります。逆に言えば、リファレンス渡しをこの関数越しに行うことは出来ません。

BAD
// Fatal error:  Only variables can be passed by reference
echo array_shift(array('YJSNPI', 'MUR', 'KMR'));
BAD
// Strict Standards:  Only variables should be passed by reference
echo array_shift($members = array('YJSNPI', 'MUR', 'KMR')); // YJSNPI
GOOD
echo call_user_func('array_shift', array('YJSNPI', 'MUR', 'KMR')); // YJSNPI

call_user_func_array 関数の利用

これまでに関数やメソッドをコールする方法は3つ紹介しました。

  • ごく普通にコールする
  • 変数にコールバック型を代入しておいてコールする
  • call_user_func 関数を用いてコールする

しかしこれらはどれも配列形式で与えられた可変長引数を渡すことは出来ませんでした。例えば array_merge 関数は

$x = array('YJSNPI');
$y = array('MUR');
$z = array('KMR');
$a = array_merge($x, $y, $z);

のように引数を受け取りますが、もしこの $x $y $z が以下のような可変長の配列で与えられていたらどうしましょう?

引数が3個の場合
$members= array(
    array('YJSNPI'),
    array('MUR'),
    array('KMR'),
);
引数が2個の場合
$members = array(
    array('YJSNPI'),
    array('MUR'),
);

個数を数えて1個1個場合分けして書きますか?

BAD
if (count($members) === 2) {
    $a = array_merge($members[0], $members[1]);
} elseif (count($members) === 3) {
    $a = array_merge($members[0], $members[1], $members[2]);
}

いいえ、そんなことをする必要は全くありません。 call_user_func_array 関数を使えば配列を引数として展開してコールすることが出来ます。但し今回に限っては array_merge 関数が引数を最低1つ必要とするため、1つ以上あるかどうかのチェックが必要です。

GOOD
if ($members) { // (count($members) > 0) とほぼ同じ意味
    $a = call_user_func_array('array_merge', $members);
} else {
    $a = array();
}

実はこの関数にも1つ弱点があります。コンストラクタがコールできないことです。以下のような func_get_args 関数を用いた可変長引数を受け取るコンストラクタがあったときに対応できません。

class KarateTeam {
    private $members;
    public function __construct() {
        $this->members = func_get_args();
    }
    public function display() {
        echo implode(', ', $this->members);
    }
}
BAD
$members = array('YJSNPI', 'MUR', 'KMR');
/* Warning:  call_user_func_array() expects parameter 1 to be a valid callback      *
 *           non-static method KarateTeam::__construct() cannot be called staticall */
$team = call_user_func_array('KarateTeam::__construct', $members);
$team->display();

これに対応するためには、 ReflectionClass::newInstanceArgs メソッドを用います。 リフレクションの意味に関してはWikipediaを参照してください。

Wikipedia - リフレクション (情報工学)

GOOD
$members = array('YJSNPI', 'MUR', 'KMR');
$reflection = new ReflectionClass('KarateTeam');
$team = $reflection->newInstanceArgs($members);
$team->display(); // YJSNPI, MUR, KMR

リフレクションは他にもたくさん用途がありますが、ここでの紹介はこの程度にしておきます。

継承

継承の基本

さて、オブジェクト指向の理解に関わる最大の山場が 継承 です。継承は、同じようなコードを二度三度と定義し直すことを回避するためにある、コード再利用の仕組みです。この定義でBから見てAのことを と呼ぶことがあります。

BはAの継承クラスであるという定義
class B extends A { } 

継承したクラスでは…

  • 親クラスのプロパティやメソッドが全て引き継がれます。
  • 引き継いだもののうち public または protected なものに限定して内部からアクセスすることが出来ます。
  • 外部からのアクセスは public に限定されます。

以下の例をご覧ください。

団員クラス
class KarateTeamMember {
    public function comment($sentence, $option = '') {
        if ($option === '') {
            return $sentence;
        } else {
            return "{$sentence}{$option})";
        }
    }
}
個別のクラス
class YJSNPI extends KarateTeamMember {
    public function say($sentence, $option = '') {
        echo 'YJSNPI「' . $this->comment($sentence, $option) . '」' . PHP_EOL;
    }
}
class MUR extends KarateTeamMember {
    public function say($sentence, $option = '') {
        echo 'MUR「' . $this->comment($sentence, $option) . '」' . PHP_EOL;
    }
}
class KMR extends KarateTeamMember {
    public function say($sentence, $option = '') {
        echo 'KMR「' . $this->comment($sentence, $option) . '」' . PHP_EOL;
    }
}
実行コード
$yjs = new YJSNPI;
$mur = new MUR;
$kmr = new KMR;
$yjs->say('じゃけん夜行きましょうね');
$mur->say('おっそうだな');
$mur->say('あ、そうだ', '唐突');
$mur->say('おいKMRァ、さっき俺等が着替えてるときチラチラ見てただろ');
$kmr->say('いや見てないです');
$mur->say('ウソつけぜったいみてたゾ', 'クレしん');
$kmr->say('何で見る必要なんかあるんですか', '正論');
$yjs->say('KMRさお前さ、さっきヌッ、脱ぎ終わった時にさ、なかなか出て来なかったよな?');
$mur->say('そうだよ', '便乗');
実行結果
YJSNPI「じゃけん夜行きましょうね」
MUR「おっそうだな」
MUR「あ、そうだ(唐突)」
MUR「おいKMRァ、さっき俺等が着替えてるときチラチラ見てただろ」
KMR「いや見てないです」
MUR「ウソつけぜったいみてたゾ(クレしん)」
KMR「何で見る必要なんかあるんですか(正論)」
YJSNPI「KMRさお前さ、さっきヌッ、脱ぎ終わった時にさ、なかなか出て来なかったよな?」
MUR「そうだよ(便乗)」

YJSNPI MUR KMR クラスではcommentという名前のメソッドは実装されていないのに利用することが出来ています。親クラスに public で存在しているからです。

継承とアクセス権の関係

private protected public
継承されるか
継承したクラス内からのアクセスが可能か ×
クラス外からのアクセスが可能か × ×

オーバーライド

図解で見る継承関係

分かりやすく図にまとめてみました。

図解

さて、ここで1つ疑問が沸いてくると思います。

なんでわざわざ (Aの)$publicP って書いてるの?
普通に parent::$publicP じゃダメなの?

一見 parent::$publicP でアクセス出来そうなのですが…

実行コード
class A {
    public $publicP = 'Aのやつ'; 
}
class B extends A {
    public $publicP = 'Bのやつ';
    public function test() {
        echo parent::$publicP;
    }
}
$b = new B;
echo $b->test();
期待される結果
Aのやつ
実際の結果
Fatal error:  Access to undeclared static property: A::$publicP

つまりこういうことなんです。

-> (アロー演算子) :: (スコープ定義演算子)
メソッド オーバーライドに基づいたアクセス クラスを明示したアクセス
プロパティ オーバーライドに基づいたアクセス
(※インスタンスプロパティに限る)
クラスを明示したアクセス
(※静的プロパティに限る)

更なる疑問が沸いてくるとすれば

何でプロパティだけそんな面倒な区別するの?…じゃあもしかして

実行コード
class A {
   public $p;
   public static $p;  
}

のように名前が被ってて形式が違う2種類のプロパティを作ったりできるの?

このようなものだと思いますが…

実際の結果
Fatal error:  Cannot redeclare A::$publicP

はい、出来ません(断言)
PHPがそういう仕様だからとして納得してください(涙目)

また、 厳密な対応表 にも記載はしていますが…

実行コード
$yj = new YJSNPI;
$yj::say('†悔い改めて†');
実行結果
Strict Standards:  Non-static method YJSNPI::say() should not be called statically
YJSNPI「†悔い改めて†」

このように外部から::を使ってインスタンスメソッドをコールすると、動作に支障は無いのですが 非推奨警告 が出ます。特に使用する意味もないので素直に -> を使いましょう。

クラスとオブジェクト(上級)

暇人向けの考察

mpyw
PHP(Laravel) / JavaScript(React/Redux/ReactNative/Vue) / MySQL あたりが得意分野なWeb系エンジニア。最近マンネリ化がひどいので Go / Kotlin / Rust / Swift あたりから何か掘り下げたいと思っている。Go は 2.x 出てから書きます。古い記事はそのまま参考にしないようにご注意ください
http://gravatar.com/mpyw
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした