PHP間でのオブジェクトの受け渡し

先日の話の続きで、PHP間でデータを受け渡しに関して。その方法をリサーチして、自分なりにいくつかの候補に絞って、それぞれについて実験してみたのでメモメモ。情報を送信する側のPHPをsend.php、受信する側のPHPをreceive.phpとし、受け渡しするデータは以下のPersonクラスのインスタンスを使った。ほんっと、もう分からないことが分からない。手さぐり。これで良いのか常に不安。PHPの作法というか文化みたいなものが全然分からないから、不自然なことしてるかもしれないけど。

<?php
// 人を表すシンプルな実験用のクラス
class Person
{
  // stringで名前
  private $_name;

  // boolで性別
  private $_sex;

  // DateTimeで誕生日
  private $_birthday;

  public function __construct($name, $sex, $birthday)
  {
    $this->_name = $name;
    $this->_sex = $sex;
    $this->_birthday = $birthday;
  }

  public function GetName()
  {
    return $this->_name;
  }

  public function GetSex()
  {
    return $this->_sex;
  }

  public function GetBirthday()
  {
    return $this->_birthday;
  }

  public function ToString()
  {
    return
      "name=". $this->_name .
      " sex=" . ($this->_sex ? "F" : "M") .
      " birthday=" . $this->_birthday->format("Y-m-d");
  }
}
?>

案1. JSON形式でPOST

先日twitterで教えてもらった方法。JSONは元々javascriptで使われてたもので、オブジェクトをリテラルに書き下すためのものらしい。このJSON形式の文字列をjavascriptのevalに食わすとオブジェクトを得ることができるのだとか。(参考:JSON | 日経 xTECH(クロステック)
ってことで、オブジェクトをJSON形式の文字列にしてくれるjson_encodeメソッドと、その逆の操作をやってくれるjson_decodeを使うと、一種のシリアライズ/デシリアライズみたいなことができそう。やれそう。
で、実際以下のコードでやってみた。

<?php
$john = new Person("john", false, new DateTime("1990-06-12"));
$jsonStr = json_encode($john);
?>

$jsonStrが"{}"になってんスけど...。調べてみると、

PHP オブジェクトを JSONエンコードすると、 オブジェクトの public プロパティがすべて JSON オブジェクトにエンコードされます。

http://framework.zend.com/manual/1.5/ja/zend.json.objects.html

publicだけって。ダメじゃん...。JSON形式ってデータをシリアライズする仕組みと考えない方が良いみたいだなぁ。この方法はボツ。

ところで、俺Lispの作成で自分もeval作ってみたけど、S式じゃない言語のevalって作んの大変そうだなぁ。構文解析とか超めんどそう。単なる想像だけど。

案2. シリアライズしてPOST

オブジェクトを文字列にシリアライズ、文字列からオブジェクトをデシリアライズする方法がPHPにもあるそうなので、それを使ってみる案。データの受け渡しはformを介してやるので、serializeメソッドの結果の文字列をhiddenで渡してやってみた。
send.php

<?php
require_once("person.php");
$john = new Person("john", false, new DateTime("1990-06-12"));
$str = serialize($john);
?>

<html>
<head><title></title></head>
<body>
<form action="receive.php" method="POST">
<?php
	echo "<input type=\"hidden\" name=\"john\" value=\"" . $str . "\">";
?>
<input type="submit">
</form>
</body>
</html>

receive.php

<html>
<head><title></title></head>
<body>
<?php
require_once("person.php");
$john = unserialize($_POST["john"]);
var_dump($john);
echo $john->ToString() . "<br />";
?>
</body>
</html>

これでやったみたら、$johnに格納されてるのはbooleanのfalseだと。デシリアライズに失敗している模様。当然ながらToString()でFatal errorになった。send.phpのページをブラウザでソース表示させてみたところ、埋め込んだシリアライズ後の文字列$strにはダブルクォーテーションが含まれていた。このせいかと思い、$strの中にシングルクォーテーションが見当たらなかったので、$strの両端をシングルクォーテーションにしてみた(激しくやっつけ)。しかし、状況は変わらなかった。良くわからんので、この案もパス。

追記

以下のメソッドについてあとで知った。

  • htmlspecialchars
  • htmlspecialchars_decode

せっかくなので、$strに対してhtmlspecialcharsした結果をHTMLに埋め込んで、receive.php側でhtmlspecialchars_decodeしてunserializeしてみたけどやっぱダメだった。んーよう分からん。

案3. ファイル経由でのシリアライズ/デシリアライズ

案2でシリアライズした内容を一時ファイルとしてサーバに保存してやってみる案。したがって、send.phpがreceive.phpに渡すのは目的のデータではなく、そのデータを保持したファイルのファイルパス。次のようなコードで実験してみた。
send.php

<?php
require_once("person.php");

$john = new Person("john", false, new DateTime("1990-06-12"));
$str = serialize($john);
$filepath = "tmp/";
$filepath .= GetUniqueFileName(".dat");

$fp = fopen($filepath, "wb");
if (!$fp)
{
  echo "failed to create file.";
  die;
}
fwrite($fp, $str);
fclose($fp);

// 一時ファイル用の重複の無いファイル名を作成する
function GetUniqueFileName($ext)
{
  $file_name=md5(uniqid(rand(), true));
  $file_name .= $ext;
  return $file_name;
}
?>
<html>
<head><title></title></head>
<body>
<form action="receive.php" method="POST">
<?php
  echo "<input type=\"hidden\" name=\"filepath\" value=\"" . $filepath . "\">";
?>
<input type="submit">
</form>
</body>
</html>

receive.php

<?php
require_once("../person.php");

$filepath = $_POST["filepath"];
if (!file_exists($filepath))
{
  header("Content-type: text/html");
  echo "file not found!";
  die;
}

$fp = fopen($filepath, "rb");
$contents = fread($fp, filesize($filepath));
fclose($fp);
?>
<html>
<head><title></title></head>
<body>
<?php
  $john = unserialize($contents);
  var_dump($john);
  echo $john->ToString() . "<br />";
?>
</body>
</html>

これは期待通り動いた。多分デシリアライズ後に用済みになった一時ファイル削除しても良さげだな。
最初、receive.phpでperson.phpをrequire_onceしてなくて、

Fatal error: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "Person" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition.

ってなエラーが出た。「_before_」の両脇のアンダーバーは何なのか良く分からないけど、メッセージの通り、unserializeする前にrequire_onceしたらちゃんとできた。__autoload()ってなんじゃろ。いつか調べる。
一時ファイルの名前の作り方は【PHP】一時ファイル用のファイル名をランダムで生成する。 | ”熱狂するシステム開発!”ブログから拝借。

案4. セッションを使う

まず、セッションって何ぞ?って状態なので、ちょっと勉強してみた。セッションとは、アプリケーション内で利用するデータを受け渡しするする仕組みで、

  • データがサーバ側で管理される
  • ネットワーク上を実データが流れない

という特徴を持っているのだそうだ。「ネットワーク上にデータが流れない」ってのがいまいち実感がつかめない。ネットワーク上にデータが流れるケースってのは一体何を指してるんだろう。読んだ本*1では、セッションと対比してクッキーの説明があったからクッキーのことを指して言ってるのかな。クッキーはクライアント側に情報を保存しておくのでセキュリティ上危険なことがあるのだそうだ。なんで危険なんだろ。つまりネットワーク上にデータが流れるからかな。良くわからん。
ってことは、つまり、案3でやったようなことを一手に引き受けつつ、ネットワーク上にデータが流れないのでより安全にデータの受け渡しができるってことかな。やってみたコードは以下。
send.php

<?php
  session_start();
  require_once("person.php");
  $john = new Person("john", false, new DateTime("1990-06-12"));
?>
<html>
<head><title></title></head>
<body>
<?php $_SESSION["john"] = $john; ?>
<form action="receive.php" method="POST">
<input type="submit"/>
</form>
</body>
</html>

receive.php

<?php
  require_once("person.php");
  session_start();
  $john = $_SESSION["john"];
?>
<html>
<head><title></title></head>
<body>
<?php echo $john->ToString(); ?>
</body>
</html>

やべえー。超簡単やんけ!これで決まりやないか!
これも最初はreceive.phpでperson.phpをrequire_onceし忘れていたので、案3の時と同じエラーが起きた。今日の朝、twitterで以下のようなやり取りをしていたので、セッションの裏でシリアライズとデシリアライズが行われていることを実感。

PHP:オブジェクト(クラス)をセッションで受け渡しする方法:serialize():シリアル化」 http://www.res-system.com/weblog/item/432

http://twitter.com/yagiey_tw/statuses/11962588250251264

@yagiey_tw これはちょっと違いますね...。セッションへの保存時にはシリアル化等は自動で行われるので、普通は serialize() を直接実行したりはしません。

http://twitter.com/meso_oishi/statuses/11964949517565952

*1:

独習PHP 第2版

独習PHP 第2版