「オブジェクトの複製」には本質的に厄介な問題をいくつも含みます。特に、オブジェクトの再帰的な複製(ディープコピー)には直感的ではない動作や単純ではない依存関係が発生しがちです。myclabs/deep-copy
はそれをいい感じに解決してくれます。
公式サイト | myclabs/DeepCopy: Create deep copies (clones) of your objects |
概要 | Create deep copies (clones) of your objects |
パッケージ名 | myclabs/deep-copy |
作者 | My C-Labs mnapoli (Matthieu Napoli) |
ライセンス | MIT License |
バージョン | v1.6.1 (2017-04-12) |
インストール
Composerでインストール可能です。
composer.phar require myclabs/deep-copy
配列のコピー
文字列および配列を含む全てのPHPの値と変数はコピーオンライト(CoW)と呼ばれる最適化戦略がとられます。
<?php $a = ['a', 'b', 'c']; $b = $a; var_dump($a === $b); //=> true $a[1] = 'X'; $b[] = 'ZZZ'; var_dump($a); //=> ["a", "X", "c"] var_dump($b); //=> ["a", "b", "c", "ZZ"] var_dump($a === $b); //=> false
$b = $a
が実行された状態では両者は同じ配列ですが、破壊的操作を行ったタイミングで別々の道を歩み始めます。つまり、二つの配列を複製したければ、PHPでは別の変数に代入してやるだけで十分なのです。
一方でRubyは、このような振舞はしません。
a = ['a', 'b', 'c'] #=> ["a", "b", "c"] b = a #=> ["a", "b", "c"] a[1] = 'X' b.push('ZZZ') p a #=> ["a", "X", "c", "ZZZ"] p b #=> ["a", "X", "c", "ZZZ"] p a == b #=> true
このように、Rubyでは単に別の変数に代入しただけでは値が複製されたことにはならず、終始において一蓮托生です。
オブジェクトのコピー
次に、stdClass
を使ってオブジェクトのコピーについて実験してみます。
<?php $book = new \stdClass; $book->name = "共産党宣言"; $book->authors = ["カール・マルクス"]; $book2 = $book; var_dump($book === $book2); //=> bool(true) $book2->authors[] = "フリードリヒ・エンゲルス"; $book->authors[0] = "Karl Heinrich Marx"; var_dump($book === $book2); //=> bool(true)
配列と同じようにやったつもりですが、違った結果になりました。
PHPでオブジェクトを複製するには別の変数に代入するだけでは不十分で、$copy = clone $obj;
のようにclone
キーワードを用ゐる必要があります。このことはPHP: オブジェクトのクローン作成 - Manualに説明があります。
再帰的なオブジェクトのコピー
ここまででオブジェクトの複製について基本的な理解が得られたところで、ここからはGitHubのmyclabs/DeepCopyのREADMEから画像を拝借しつつ説明を進めていきます。
アルファベット1文字のクラスでとてもわかりにくいのですが、以下のようなクラスがあったとします。
<?php class A { /** @var string */ public $name; /** @var B */ public $b; /** @var C */ public $c; public function __construct($name) { $this->name = "Hi, I am {$name}."; } } class B { /** @var string */ public $name; /** @var C */ public $c; public function __construct($name) { $this->name = "Hi, I am {$name}."; } } class C { /** @var string */ public $name; public function __construct($name) { $this->name = "Hi, I am {$name}."; } }
これらのクラスを使ってコードを書いてみますね。
<?php $c = new C('Charlie'); $b = new B('Bob'); $b->c = $c; $a = new A('Alice'); $a->c = $c; $a->b = $b;
これをグラフにすると以下のような状態です。
次に、これを「まるごと」複製してみたいと思ったとします。まるごと複製とは、ここでは$a
を$a2
に複製したとして、$a2->c
のオブジェクトを変更したとしても$a->c
のオブジェクトには反映されたくないといふことだとします。$a->b
についても同様。
<?php $a2 = clone $a; // => A {#192 // +name: "Hi, I am Alice.", // +b: B {#200 // +name: "Hi, I am Bob.", // +c: C {#173 // +name: "Hi, I am Charlie.", // }, // }, // +c: C {#173}, // }
ほうほう。では$a2->c-name
に別の文字列を入れてみたらどうかな。
<?php $a2->c->name = 'Charlotte'; // => "Charlotte" $a // => A {#178 // +name: "Hi, I am Alice.", // +b: B {#200 // +name: "Hi, I am Bob.", // +c: C {#173 // +name: "Charlotte", // }, // }, // +c: C {#173}, // } $a2 // => A {#192 // +name: "Hi, I am Alice.", // +b: B {#200 // +name: "Hi, I am Bob.", // +c: C {#173 // +name: "Charlotte", // }, // }, // +c: C {#173}, // }
$a->c
の方も変っちゃったよだめじゃん…
これはグラフにすると以下のような状態です。
__clone()
大戦
今度もPHP: オブジェクトのクローン作成 - Manualを参考に__clone()
マジックメソッドを定義してみることにします。またクラス定義ですが、変更点は__clone()
が増えてるだけです。
<?php class A { /** @var string */ public $name; /** @var B */ public $b; /** @var C */ public $c; public function __construct($name) { $this->name = "Hi, I am {$name}."; } public function __clone() { $this->b = clone $this->b; $this->c = clone $this->c; } } class B { /** @var string */ public $name; /** @var C */ public $c; public function __construct($name) { $this->name = "Hi, I am {$name}."; } public function __clone() { $this->c = clone $this->c; } } class C { /** @var string */ public $name; public function __construct($name) { $this->name = "Hi, I am {$name}."; } }
よし、これなら動くかな…?
<?php $a2 = clone $a; $a2->c->name = 'Charlotte'; // => "Charlotte" $a // => A {#209 // +name: "Hi, I am Alice.", // +b: B {#218 // +name: "Hi, I am Bob.", // +c: C {#213 // +name: "Hi, I am Charlie.", // }, // }, // +c: C {#213}, // } // >>> $a2 // => A {#207 // +name: "Hi, I am Alice.", // +b: B {#199 // +name: "Hi, I am Bob.", // +c: C {#210 // +name: "Hi, I am Charlie.", // }, // }, // +c: C {#197 // +name: "Charlotte", // }, // }
今度は$a->c
と$a2->c
の状態はきっちり切り離せたようです。しかし今度は$a->c
と$a->b->c
の関係が奇妙なことになってしまひましたね。
やれやれ…
myclabs/deep-copyの出番だ
__clone()
では問題が解決しないことがわかったので、クラス定義は元の状態に戻します。
<?php use DeepCopy\DeepCopy; $c = new C('Charlie'); $b = new B('Bob'); $b->c = $c; $a = new A('Alice'); $a->c = $c; $a->b = $b; // Deepcopyを使ってオブジェクトのコピー $deepCopy = new DeepCopy(); $a2 = $deepCopy->copy($a); $a2->c->name = 'Charlotte'; // => "Charlotte" $a // => A {#190 // +name: "Hi, I am Alice.", // +b: B {#192 // +name: "Hi, I am Bob.", // +c: C {#186 // +name: "Hi, I am Charlie.", // }, // }, // +c: C {#186}, // } $a2 // => A {#188 // +name: "Hi, I am Alice.", // +b: B {#165 // +name: "Hi, I am Bob.", // +c: C {#202 // +name: "Charlotte", // }, // }, // +c: C {#202}, // }
やった、ばっちりだ!
このライブラリは利用すべきなの?
オブジェクトの複製にどのような手法をとるのが望ましいかは、場合によります。そもそも再帰的な複製(ディープコピー)は必要なく、浅い複製(シャローコピー)で十分だといふケースもあります。myclabs/deep-copy
は直感に反しないディープコピーの手法として、良い選択肢のひとつです。
上記のようなオブジェクトの複製に伴った理窟がいまひとつ理解できない場合は、オブジェクトの利用を諦めて連想配列に落としてみるといったこともPHPらしい、割り切った解決法ではあります。筆者が業務として開発に携るpixiv.netでは、オブジェクトはほとんど利用しません。