WordPressのプラグイン作成 第二回

WordPress のプラグイン作成 — $wpdb 編 その1 –

posted at 2014/01/15 by denpoya@コワーキングスペース町田

今回は、WordPressにデータベースのテーブルを作成してもらうことが主なテーマです。その時使う$wpdbやWordPressの関数dbDelta()update_option()get_option()の使い方にも触れておきます。

さらに、テーブルの数が複数になった時にforeach文で回すための工夫もしておくことにしましょう。バージョンが上がって、テーブルを増やさなければならなくなった時でも簡単に対処できるようになるでしょう。

WordPressにテーブルを作ってもらう

WordPressにテーブルを作ってもらうのはいつどの場面でしょうか?プラグインを「有効化」した時の1回だけでよいですか?

プラグインの新しいバージョンを作成する際、別のテーブル構造が必要になったらアップグレード用関数を作成することになるでしょう。フィールド属性を変更するだけでなく、新しいテーブルを追加することもあるかもしれません。そのようなことを念頭に置いてコーディングしていきます。

WordPressが通常使っているテーブルではなく、私たちのプラグイン専用のテーブルを「有効化」のタイミングで作成することにします。本当に専用のテーブルが必要かどうかは検討すべきでしょう。このプラグインには必要である、という設定でお話を進めていきます。

WordPressをインストールするときにテーブル名のプレフィックス(接頭辞)を指定できました。私たちのテーブル名にも同じプレフィックスをつける必要があるでしょう。デフォルトは「wp_」ですがインストールする人によって異なるプレフィックスになっているかもしれません。

まず、第1段階として

  1. 「テーブル名に付けられているプレフィックスを私たちのテーブルにも付ける」
  2. 「私たちのテーブルが既に存在しているかを確認しておく」

この2つからコーディングしていきます。必要なものは$wpdbです。

WordPressのデータベース操作用のクラス関数 wpdb

WordPressにはデータベース操作用のクラス関数が用意されています。

しかしそれを直接使うのではなく、WordPressにはデータベースと対話するために設定されたクラスをインスタンス化した$wpdbというグローバル変数が用意されているのでこのグローバル変数$wpdbを使うのが良いようです。

そのためにはグローバル宣言をします。

global $wpdb;

$wpdbの持つメソッドのうちよく使うのが、get_var('query',column_offset,row_offset)get_results('query', output_type)そしてquery('query')といったSQL文を発行するメソッド、その時にSQLインジェクション攻撃からクエリを保護するために使うprepare( 'query'[, value_parameter, value_parameter ... ] )メソッドとSQLエスケープのためのescape($user_entered_data_string)ではないでしょうか。説明は関数を使う時にしましょう。

テーブル名にプレフィックスをつける

テーブル名に付けられているプレフィックスは、

$wpdb->prefix;

という変数に格納されています。例えば、「example_table」という名前にプレフィックスをつけてテーブル名にしたいならば、

$table_name = $wpdb->prefix . 'example_table';

のようにします。

テーブルが既に存在しているかどうかを確認する

テーブルが既に存在しているかどうかを確認するのには、

$is_db_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));

のように「SHOW TABLES LIKE テーブル名」クエリを発行します。prepareメソッドによって、「%s」には第2引き数の文字列がその箇所に挿入されます(2つ目の「%s」があったら第3引き数の値が挿入されます)。

get_varメソッドはデータベースから変数を一つ返します。変数は一つしか返ってきませんが、クエリの結果はあとから使うことができます。結果にマッチするものがない場合、NULL が返されます(関数リファレンス/wpdb Classを参照、以下同様)。

テーブルがないときには作成するようにします。

CDPluginクラスのactivate()メソッドを実装

前回作成したCDPluginクラスのactivate()メソッドを編集します。既に述べたことから下のようになるでしょう。

  • CDPlugin.php
<?php
class CDPlugin {
    function __construct() {
    }

    function activate() {
        $table_name = $wpdb->prefix . 'main_master';
        $is_db_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));

        if ($is_db_exists == $table_name) {
            return;
        }

        // ここでSQLクエリの実行する

    }
}
SQLクエリの実行

直接SQLクエリを実行($wpdb->query($sql))してもよいのですが、wp-admin/include/upgrade.phpで定義されているdbDelta関数を使う方がよいようです。dbDelta関数現在のテーブル構造を走査し、作成予定のテーブルと比較、必要に応じてテーブルを追加・修正します。更新にはとても便利な関数です(Creating Tables with Plugins(日本語版)を参照)。ただし、使用するには少しばかり癖があるようです。

参考にさせていただいた上記リンクとその最新版であるCreating Tables with Plugins(最新英語版)のページを見ると日本語版よりも癖がもう一つ増えて4つ項目が並んでいます。

  1. 1行につき1つのフィールドを定義してください。 〔訳注:一つの行に複数のフィールド定義を書くことは出来ません。さもなくば ALTER TABLEが正しく実行されず、プラグインのバージョンアップに失敗します。 〕
  2. PRIMARY KEYと主キーの定義の間には二つのスペースが必要です。〔訳注:原文 "You have to have two spaces between the words PRIMARY KEY and the definition of your primary key."〕
  3. INDEXという言葉ではなく、KEYという言葉を使う必要があります。
  4. フィールド名をアポストロフィやバッククォートで囲ってはいけません。〔原文:"You must not use any apostrophes or backticks around field names. "〕

このことに気を付けてSQL文を次のように作ります。dbDelta関数を使うには、wp-admin/include/upgrade.phpをインクルードする必要があります。

発行するSQL文を次のようにし、dbDelta関数を実行させます。

$sql = "CREATE TABLE " . $table_name . " (
     id mediumint(9) NOT NULL AUTO_INCREMENT,
     name tinytext NOT NULL,
     url VARCHAR(55) NOT NULL,
     created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '作成日',
     modified timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '修正日',
     UNIQUE KEY id (id)
     );";

require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
バージョン管理

合わせて私たちのテーブルのバージョン管理の仕組みも用意します。プラグインを一度「停止」してもらい、再び「有効化」することで今編集しているactivate()が実行されるのでデータベースに変更があった時だけdbDelta関数を呼び出しテーブルが追加更新されるようにするためです。

私たちのテーブルのバージョン番号は、WordPressの持つ設定を保存するテーブルに登録させてもらうことにしましょう。

独立した、名前の付いたデータ (「設定」) を WordPress データベースに保存し利用することができます。設定値の取り得る形式は、文字列 (string)、配列 (array)、もしくは PHP オブジェクトです (オブジェクトは、保管時は「シリアライズ」もしくは文字列に変換され、読出時にオブジェクトに戻されます)。オプション名は文字列で、他にない唯一のものでなければなりません。なぜなら、WordPress や他のプラグインと衝突しないためです(プラグインの作成を参照)。

使う関数は下の2つ、add_option()を利用する必要はありません。

update_option( $option, $new_value )

optionsデータベーステーブルから、指定したオプションの設定値を更新または作成します。オプションの設定値はインサートされる前に$wpdb->escapeでエスケープされます。また、指定された
オプション名が存在していないときにはadd_option( $option, $new_value )が実行されるので、add_option( $option, $new_value )の代りに使うことができます。

$option_name
    (文字列) (必須) 更新したい設定の名前。有効なデフォルトオプション値は、Option Referenceに一覧があります。

        初期値: なし

$newvalue

    (混合) (必須) 設定の新しい値。この値には、整数、文字列、配列やオブジェクトを取ることができます。

        初期値: なし
get_option( $show, $default )

optionsデータベーステーブルから、指定したオプションの値を取得する安全な方法です。希望するオプションが存在しない場合は、値が関連付けされず、FALSE が返されます。

 $show
    (文字列) (必須) 取得するオプションの名前。有効なデフォルトオプション値は、Option Referenceに一覧があります。

        初期値: なし

$default
    (混合) (オプション) 値が返されない(データベースにオプションが存在しない)場合のデフォルト値。

        初期値: false
完成したメソッド

SQL文は、別メソッドからの戻り値となるようにしました。

<?php
class CDPlugin {
    // 現在のテーブルのバージョン。
    // 数値を上げるとテーブルを更新する。
    // (テーブルが存在していて)上げなければ何もしない。
    private $version = 0.1;

    function __construct() {
    }

    public function activate() {
        $db_version = get_option('cdq_db_version', 0);

        $table_name = $wpdb->prefix . 'main_master';
        $is_db_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));

        if ($is_db_exists == $table_name && $db_version >= $this->version) {
            return;
        }

        require_once ABSPATH . "wp-admin/includes/upgrade.php";
        dbDelta($this->$getSql($table_name));

        update_option("cdp_db_version", $this->version);
    }

    private function getSql($table_name) {
        return <<<EOS
CREATE TABLE  $table_name (
     id mediumint(9) NOT NULL AUTO_INCREMENT,
     name tinytext NOT NULL,
     url VARCHAR(55) NOT NULL,
     created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '作成日',
     modified timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '修正日',
     UNIQUE KEY id (id)
);
EOS;
    }

}

複数のテーブルに対応

テーブルが1つだけなら良いのですが、複数のテーブルを使うプラグインでは、先のactivate()がテーブルの数だけ必要になります。そんな時はforeachで回したくなります。そのための工夫をしてみましょう。

テーブル名の識別とSQL文

先ず、テーブルを識別できるようにします。新しいテーブルを追加するときにここに追加記入します。

private $tables = array(
    "master_1",
    "detail_1",
    "master_2",
    "detail_2",
);

そしてもう一つ新しいテーブルを追加するときに記述しなければならないのはSQL文です。この二つを記述するだけで新しいテーブルを追加できるようにします。

それには、SQL文を返すメソッド名に規則を作ります。すなわちメソッド名に、上で記入したテーブルの識別を使って、例えば、こんな感じにします。

private function master_1Sql($table_name) {return "CREATE TABLE $table_name ・・・"}
private function detail_1Sql($table_name) {return "CREATE TABLE $table_name ・・・"}
private function master_2Sql($table_name) {return "CREATE TABLE $table_name ・・・"}
private function detail_2Sql($table_name) {return "CREATE TABLE $table_name ・・・"}

"テーブルの識別" . "Sql"がメソッド名になっています。新しいテーブルを追加するときに新たに記述するのはこれだけでそれ以外に修正する必要はありません。

プレフィックスをつける

実際にCREATEされるテーブル名(プレフィックスがつく)を作ります。

private function getTableNames() {
    global $wpdb;
    foreach ($this->tables as $name) {
        $this->table_names [$name] = $wpdb->prefix . $name;
    }
    return $table_names;
}
クエリを実行

そして、各テーブルごとにSQL文を発行し、クエリを実行するよう以下のコードをactivate()メソッドに埋め込みます。

require_once ABSPATH . "wp-admin/includes/upgrade.php";

$table_names = $this->getTableNames();
foreach ($table_names as $name => $table_name) {
     $sqlfunc = $name . "Sql";
    dbDelta($this->$sqlfunc ($table_name));
}

今回は、ここまで。

次回は、「$wpdb 編 その2」として、$wpdbの使い方、エラー処理(といっても、エラーメッセージを受け取るわけですが)などに触れたいと思います。

WordPressのプラグイン作成 第一回

WordPress のプラグイン作成 はじめの一歩

posted at 2014/01/12 by denpoya@コワーキングスペース町田

最初のお話はプラグインを作成するための準備段階として、

  1. WordPressに「これはプラグインですよ」と認めてもらい、
  2. プラグインを有効化したときに実行してもらいたいメソッドactivate()を作り、それをWordPressに実行してもらう

というところまでお話していきたいと思います。activate()は、関数として実行させても良いですし、クラスで定義されたメソッドであっても良いので、両方の場合を紹介します。

そして、次回のテーマとしては、このactivate()メソッドの役割として、プラグインに必要なデータベースのテーブルを作ってもらうことにします。

WordPressにプラグインとして認めてもらう

プラグインを配置するディレクトリとPHPファイルを作っていきます。

プラグインの名前

では、作りたい機能からプラグインの名前を決めましょう。すでにある他のプラグインと同じ名前にならないように気をつけます。ここでは、私たちの社名「株式会社 C & D」を使って、「CD Plugin」をプラグイン名とします。

プラグイン・ファイル

次は、選択したプラグイン名に由来する名前のPHPファイルを作ります。プラグインをインストールする人は複数のプラグインを利用することでしょう。そのような自分以外のプラグイン・ファイル名と同じファイル名を使うことはできません。また、プラグインのディレクトリ名も同様に重ならない唯一の名前にします。

wordpress
:
|-- wp-content
|   |-- plugins     <----- プラグインを配置するディレクトリ
|   |   |-- cd_plugin   <----- 私たちのプラグインのディレクトリ
|   |   |   |-- CDPlugin.php
|   |   |   |-- cd_plugin.php
|   |   |   |--images
|   |   |   |   `-- cd_image.jpg
|   |   |   |--css
|   |   |   |   `-- style.css
|   |   |   |--js
|   |   |   |   `-- jquery.js

その中にはプラグイン・ファイルだけでなくプラグインで利用するCSSファイル、JavaScriptファイルや画像ファイルなどを配置するディレクトリもあることでしょう。

標準プラグイン情報

WordPress にプラグインの存在を認識させ、プラグイン管理画面に表示させる「標準プラグイン情報」は、プラグインのメインになるPHPファイルの先頭に記述します。それを私たちのcd_plugin.phpとします。

<?php
/*
 Plugin Name: (プラグインの名前)
 Plugin URI: (プラグインの説明と更新を示すページの URI)
 Description: (プラグインの短い説明)
 Version: (プラグインのバージョン番号。例: 1.0)
 Author: (プラグイン作者の名前)
 Author URI: (プラグイン作者の URI)
 License: (ライセンス名の「スラッグ」 例: GPL2)
*/

先のディレクトリ構造にはPHPファイルを二つ置きました。cd_plugin.phpCDPlugin.phpです。これらのうちcd_plugin.phpの方に「標準プラグイン情報」を書いてみましょう。
CDPlugin.phpやその他のディレクトリなどはなくてかまいません。「標準プラグイン情報」を記述することで、WordPressにプラグインであることを認めてもらえます。

  • cd_plugin.php に「標準プラグイン情報」を下のように記述しました。
<?php
/*
 Plugin Name: CD plugin
 Plugin URI: http://www.nothing.pom/cd_plugin
 Description: WordPress 初めてのプラグイン
 Author: コワーキング・スペース町田「小町」
 Version: 0.1
 Author URI: http://www.nothing.pom
*/

そして、WordPressの管理者としてログインし管理者メニューの[プラグイン]をクリックすると

プラグインとして登録

このようにプラグインの仲間入りができます。

プラグインが有効化された時に実行させる関数を登録する

『▲▲をしたタイミングで、◎◎をしてもらう。』そのための仕組みをプラグインフックといいます。フックにはたくさんの種類が用意されているので適切なフックを探す必要があります。プラグイン API/アクションフック一覧 が役に立つかもしれません。

関数register_activation_hook($file, $function)

これは、プラグインを有効化したときに実行してもらいたい関数を登録する関数です。
引数は、次のように説明されています(上記リンク参照)。

引数
$file (string) (必須)
    wp-content/pluginsディレクトリにあるメインプラグインファイルへのパス。フルパスが有効です。
    初期値: なし
$function (callback) (必須)
    プラグインが有効化されたときに実行される関数。PHPにおける疑似的な型callbackとして許可されたものである必要があります。
    初期値: なし
使用例

phpファイル自身の中に実行したい関数を記述している、またはインクルードされている場合には

register_activation_hook( __FILE__, 'myplugin_activate' );

のように登録します。

ここで「__FILE__」は、自分自身のPHPファイルのパス名が定義されている定数です。

また、クラスを作りそのメソッドを実行させたい場合には、まずクラスファイルをインクルードしておき、newメソッドでインスタンスを作成します。そして実行させたいメソッドを次のように登録することができます。

require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . "クラスファイル.php";
$clazz = new クラス名();

register_activation_hook(__FILE__, array($clazz, "クラスのactivateメソッド名"));

では、私たちの例で具体的に作っていきましょう。

  • CDPlugin.phpに以下のコードを記述します。
<?php
class CDPlugin {
    function __construct() {
    }

    public function activate() {
        echo "初めてのプラグイン"; <-- これはやってはいけない。怒られます。
    }
}
  • cd_plugin.php にコードを追加します。

CDPlugin.phpを読み込ませて、インスタンスを作ります。

<?php
/*
 Plugin Name: CD plugin
 Plugin URI: http://www.nothing.pom/cd_plugin
 Description: WordPress 初めてのプラグイン
 Author: コワーキング・スペース町田「小町」
 Version: 0.1
 Author URI: http://www.nothing.pom
*/

require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . "CDPlugin.php";
$cdp = new CDPlugin();

register_activation_hook(__FILE__, array($cdp, "activate"));

array()を使ってクラスとそのメソッド名を指定することで、プラグインが有効化されたタイミングで"activate"メソッドを実行するフックが登録されます。

ただし、「有効化」をしてこれを実行させるとメッセージが出てWordPressに怒られます。

有効化をしたとき

今回はここまで。

次回は、activate()メソッドの中を編集していきます。目標は、プラグインに必要なデータベースのテーブルを作成することです。その際、機能のバージョンアップに伴うテーブルの変更にも気を配っていきたいと思います。

PHPの参照をめぐる冒険

PHPの参照をめぐる冒険

知久です。

少しばかりPHPから離れてしまっていることもあって(最後にやったPHP案件はまだ数年前だけど、その1つ前はバージョン3や4の時代・・・w)、つい先日Facebook上で「Copy on write」っていうのがあって、特にPHPでは参照渡しなど使わなくても大丈夫などと教えてもらったりしたのですが、その時「やはり技術者は日々勉強だな」と反省し、少しPHPの参照周りを調べてみました。今回はちょっとその辺の報告です(間違いのご指摘、歓迎します)。

とりあえず、結論は以下のようです(説明はその後しますw)。

php_reference

そして、この結論をもう少し掘り下げてみます。

そもそも関数の引数は?

関数の呼び出しの種類として、よく聞かれる言葉に

  • Call by value(値渡し)
  • Call by reference(参照渡し)
  • Call by sharing(共有渡し)→参照の値渡しとも言われています

というものがあります。この中で「Call by reference(参照渡し)」と「Call by sharing(共有渡し)」の違いが多少難しく、Google先生に聞いても「わかっていない人が多い」という文言が目立ちますので、ここは気合いの入れどころのようですw。

じゃ、「参照渡し」ってなんだ?あたりを読んで(中の引用のところ)、僕なりに解釈すると

渡された仮引数が「左辺値」として使われる場合、
言い換えれば、「左辺値」として扱われることを意図した関数の呼び出しは
「Call by reference(参照渡し)」であり、
意味がない場合「Call by value(値渡し)」である。

という感じでしょうか。

function func($value)//←仮引数
{
    $value = うんたらかんたら;
}

という使い方をしているのが、くどいですが、「Call by reference(参照渡し)」だと解釈できます。

ちなみに「実引数は呼んでいる側の引数」で「仮引数は関数側で受ける引数」です。

もっと具体的に見ていきましょう(「渡し」という言い方が流通してしまっているので「参照による呼び出し」のような「呼び出し」を使わず、「渡し」を使っていきます)。

値渡し

下のコードを見て下さい。

<?php
function func1($value)//←仮引数
{
    $value = $value * 2;
}

function func2($value)
{
    $data = $value * 2;
    return $data;
}

$num = 123;
func1($num);//←実引数
echo $num."\n";// => 123
$num = func2($num);
echo $num."\n";// => 246

function func1($value)//←仮引数
{
    $value = $value * 2;
}

$num = 123;
func1($num);//←実引数

で呼ばれ、実引数で渡した123を2倍にして仮引数を上書していますが、その上書きが、関数の外のecho文では活かされていません。123のまま出力されています。

もちろん、func2のようにreturn文で$numを書き換えれば、$numに反映はできますが、この場合、仮引数の$valueは右辺としての意味はありますが、左辺としての意味はありません!

とうわけで、これらの関数の呼び出しでは仮引数を左辺においても意味がなく、「Call by value(値渡し)」ということになります。

参照渡し

次は前のとほんのちょっとだけ違うコードです。単に仮引数の前に「&」が付いてるのが違うだけです。

<?php
function func(&$value)//←仮引数
{
    $value = $value * 2;
}

$num = 123;
func($num);
echo $num."\n";// => 246

今度の場合は仮引数を左辺にして「値を2倍に」した結果が$numに反映されています(246が出力されています)。つまり、実引数を仮引数に渡し、これを左辺に利用して意味がある使い方になり「Call by reference(参照渡し)」ということになります。

C++やPascalなども同様のことができるようですが、Javaではできません(「Javaではできないだと〜」と思った人は次の章を読みましょうw)。PHPではこの&を仮引数に付けることにより、参照渡しが実現できるということです。

ちなみにJavaは次の「Cal by sharing(共有渡し)」であり、PHPもオブジェクトを代入した時の場合などもそうです。JavascriptやPython、Rubyなども「共有渡し」だそうなので、ここは重要です。

共有渡し

ちょっと長目ですが、次のコードを見て下さい。まずはRefTestというクラスは2つのインスタンス変数があり、コンストラクタでこれらの変数を初期化するという、とっても単純なクラスを定義しています。

通常はインスタンス変数はpublicではなく、privateでクラスを作るべきですが(そしてgetter、setterを作る)、話を単純にするために外からでもアクセスできるpublic変数にしています。

<?php
// コンストラクタとpublicな変数が2つある単純なクラス
class RefTest
{
    public $value1;
    public $value2;

    function __construct($v1, $v2)
    {
        $this->value1 = $v1;
        $this->value2 = $v2;
    }
}

このクラスをnewしていくつかの関数にそのオブジェクトを渡して、試してみましょう。

まずはref1関数。

// $refのメンバ変数value1を10に変更
function ref1($ref)
{
    $ref->value1 = 10;
}

$obj = new RefTest(1, 2);
// コンスタラクタの引数がそのまま出力
echo $obj->value1 . "\n"; // => 1
echo $obj->value2 . "\n"; // => 2

// ref1メソッドでvalue1の方が10に変更される
ref1($obj);
echo $obj->value1 . "\n"; // => 10
echo $obj->value2 . "\n"; // => 2

$obj = new RefTest(1, 2);
ref1($obj);

で、RefTestクラスをnewし、ref1関数に$objを渡します。関数の中身は

function ref1($ref)
{
    $ref->value1 = 10;
}

なので、value1の方を10に変更され、

echo $obj->value1 . "\n"; // => 10
echo $obj->value2 . "\n"; // => 2

となります。これは「Cal by sharing(共有渡し)」で、「「Cal by reference(参照渡し)」ではありません。どうして参照渡しじゃないかがポイントなんですが、次のref2関数の場合と比べてみましょう。

// 新しいオブジェクトで仮引数を上書き
function ref2($ref)
{
    $ref = new RefTest(100, 200);
}
$obj = new RefTest(1, 2);

// 仮引数に新しいRefTestオブジェクトをnewして代入←しかし、出力は前のまま
ref2($obj);
echo $obj->value1 . "\n"; // => 1
echo $obj->value2 . "\n"; // => 2

このref2の場合、関数の中で仮引数を新しくnewしたRefTestオブジェクトで上書きしています。そして、その時のコンストラクタの引数が(100,200)なのですが、関数実行後のvalue1やvalue2の出力はそのままです。

何が違うかというと下のように、仮引数である$refのメンバー変数を変更しているということと(左)、仮引数自体を書き換えていることです

$refのメンバー変数を変更している 仮引数自体を書き換えている


$ref->value1 = 10;


$ref = new RefTest(100, 200);

つまりPHPでは、変数にオブジェクトが格納されているという条件下では、明示的に&を仮引数に付けずに普通に関数を呼び出した場合、オブジェクトのリファレンスが実引数からコピーされて仮引数に渡り、別々の変数が同じオブジェクトを見ているのですが、仮引数を新しいオブジェクトで関数内で書き換えると、別々のオブジェクトを指すようになります。図にすると下のような感じでしょうか(上の2つの図)。

ref2

これは、結局アドレスという値がコピーされて仮引数に渡されるので「参照の値渡し」とも呼ばれ、人によっては「値渡し」という人もいますが、オブジェクトのメンバ等にはアクセスして、変更等もできるので、若干「値渡し」とは挙動が違いますので、ここでは共有渡しと呼んでおきます。

参照渡しと比べておきましょう。

// 新しいオブジェクトで仮引数を上書き(ただし参照渡し!)
function ref3(&$ref)
{
    $ref = new RefTest(100, 200);
}
// 参照渡しの仮引数を新しいRefTestオブジェクトをnewして代入←出力が変わる
ref3($obj);
echo $obj->value1 . "\n"; // => 100
echo $obj->value2 . "\n";// => 200

繰り返しですが、「function ref3(&$ref)」のように「&」が付いているだけで、アドレスがコピーされるのではなく、実質同じ変数になります(仮引数が実引数のエイリアスになるイメージでしょうか?変数のアドレス自体が渡るという言い方をする人もいます)。したがって、仮引数を新しいオブジェクトで関数内で書き換えると、どちらの変数も同じ新しいオブジェクトを指すようになります。

「実質」と言っているのは、内部的にPHPがどのように処理しているかは深入りしないということですw(「php zval」というキーワードで検索すると良いかもしれません)。

「値渡し/参照渡し/共有渡し」のまとめ

ここでまとめておきます。PHPでは、次のようになっています。

  • オブジェクトを入れた変数以外は「値渡し」
  • オブジェクトを入れた変数は「共有渡し」
  • 関数の仮引数に&を入れた場合は「参照渡し」

ということになります。以下、3つで違うことを頭に入れる必要がありそうですねw

共有渡しでも、オブジェクトの中身(メンバ変数)を変えることが可能 共有渡しでは、仮引数自体を書き換えても意味がない(左辺になれない) 参照渡しだと、左辺になれる(つまり実引数の方も変更される)
function ref1($ref)
{
    $ref->value1 = 10;
}
function ref2($ref)
{
    $ref = new RefTest(100, 200);
}
function ref3(&$ref)
{
    $ref = new RefTest(100, 200);
}
Copy-On-Write

さて、話がさらに複雑になります。上記でまとめた1番の項目オブジェクトを入れた変数以外は「値渡し」ですが、その値の内容を変えずに、参照だけをするだけなら「共有渡し」的に渡すというのが「Copy-On-Write」の考え方です。例えば、次のような大きな配列に値を入れた場合、値渡しするとメモリーがもったいないよね(大きな配列が2つになるので)、という発想です。

次のようなコードを実行すると、①のところをコメントアウトしていると普通に実行できますが、コメントを取るとメモリー不足のエラーが起こります。

<?php
#メソッド1
# 100万個の配列の値をすべて2倍にする
function func($arr)
{
    $end = count($arr);
    for ($i = 0; $i < $end; $i++) {
        // $arr[$i] = $arr[$i] * 2; // ①
    }
    return $arr;
}
# ここからMainメソッド
$array = Array();
# 100万個の配列作成
for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

$array = func($array);

つまり、私のMacBook Airでは、デフォルトを倍にしたphp.ini内の「memory_limit」が

memory_limit = 256M

で①が実行されなければ、エラーもなく実行できるけれど、①が実行されると

Allowed memory size of 268435456 bytes exhausted

というエラーが起こる、ということです。

どうしてかというと、

        $arr[$i] = $arr[$i] * 2; // ①

では、配列に入っている値を2倍にして再格納しているので、渡された配列の中身を変えています。つまり渡された変数が変更されない限り、共有渡し的にになるわけですが、今回の例では実際には変更しているので、

この

$array = func($array);

で「func」というメソッドが呼ばれた時配列自体がコピーされて、100万個の配列が2個でき、メモリー不足になったわけです。「Copy On Write」はまさに「書きこむ時にはコピーする」ということなわけです。

ただ、今回何度も出てきた

function func(array &$arr)

&$arrの「&」をつけることにより、参照渡しになり、配列が2つできないので(コピーされないので)、先ほどの条件でもメモリー不足にはなりません。

とはいえ、「Copy On Write」の登場により、今回の例のように渡した配列の中身を変更したりしない限り、「&」を付けなくても大丈夫ということになります

このことによって、あまり「参照渡し」を利用する機会がなくなってきている(あるいはむしろバグの温床にもなる)というようなことを言っている人もいて、概ね、私も賛成です。ただ積極的に参照渡しにしないと困るという状況以外では、利用しない方が良いと思うのですが、一方で、配列の中身を変更する場合はコピーされるということでもあり、メモリー不足になってエラーになってしまう場合もあり得るということも事実です。

そもそも100万個の配列を扱うこと自体ダメじゃん、ということは言えますし、512Mではエラーにならないので、そうすることも可能ですが、それでも状況によっては、やはり、どうしても色々な理由で—予算の関係でメモリーを増やせないとか、配列をいくつかに分解して処理すると遅くなってしまうなど—100万個の配列を扱いたいというようなケースも現実にはあり、その場合は参照渡しを検討すべきかな、と思うわけです。

おまけ(どっちが速い?)

最後に参照渡しと値渡しでどれくらい速度が違うんだろうかな、と実験してみました。これは、与えられた条件で違ってくるものなので、PHPの関数では、参照渡しよりもreturnしたほうが速い!?では、どでかい文字列を参照変数に代入するのはパフォーマンス的に遅いというようなことを言っています。が、このリンク先の例では、「参照渡し」するような積極的な意義はなさそうです(先ほど、メモリー不足のような状況が起こるケースでは、と言いましたが、ここの例では空文字の変数を渡しており、単にリターン文で書き換えればもともと良い、というようなケースに見えますw)。

<?php
// reference.php
function func(array &$arr)
{
  $end = count($arr);
  for ($i = 0; $i < $end; $i++) {
        $arr[$i] = $arr[$i] * 2;
    }
}

for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

func($array);

というような参照を渡した場合の結果が下のよう。

$ php reference.php 

real    0m0.481s
user    0m0.381s
sys     0m0.098s

また、値渡しの場合の下のようなコードでは

// value.php
function func(array $arr)
{
  $end = count($arr);
  for ($i = 0; $i < $end; $i++) {
        $arr[$i] = $arr[$i] * 2;
    }
    return $arr;
}

for ($i = 0; $i < 1000000; $i++) {
    $array[$i] = $i;
}

$array = func($array);

実行結果は、少しだけ、参照を渡したときよりも遅いですね。

time php value.php 

real    0m0.567s
user    0m0.449s
sys     0m0.116s

関数の値渡しと参照渡しどちらが速い?でも、似たような結果になっていますw

今回はこれでおしまい。