超PHPerになろう

Enjoy PHP Programming

オブジェクトをいい感じに複製(クローン)する [myclabs/deep-copy]

「オブジェクトの複製」には本質的に厄介な問題をいくつも含みます。特に、オブジェクトの再帰的な複製(ディープコピー)には直感的ではない動作や単純ではない依存関係が発生しがちです。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) Packagist

インストール

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に説明があります。

再帰的なオブジェクトのコピー

ここまででオブジェクトの複製について基本的な理解が得られたところで、ここからはGitHubmyclabs/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;

これをグラフにすると以下のような状態です。

f:id:zonu_exe:20160811234717p:plain

次に、これを「まるごと」複製してみたいと思ったとします。まるごと複製とは、ここでは$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の方も変っちゃったよだめじゃん…

これはグラフにすると以下のような状態です。

f:id:zonu_exe:20160812002853p:plain

__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の関係が奇妙なことになってしまひましたね。

f:id:zonu_exe:20160812002807p:plain

やれやれ…

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},
//    }

やった、ばっちりだ!

f:id:zonu_exe:20160812005805p:plain

このライブラリは利用すべきなの?

オブジェクトの複製にどのような手法をとるのが望ましいかは、場合によります。そもそも再帰的な複製(ディープコピー)は必要なく、浅い複製(シャローコピー)で十分だといふケースもあります。myclabs/deep-copyは直感に反しないディープコピーの手法として、良い選択肢のひとつです。

上記のようなオブジェクトの複製に伴った理窟がいまひとつ理解できない場合は、オブジェクトの利用を諦めて連想配列に落としてみるといったこともPHPらしい、割り切った解決法ではあります。筆者が業務として開発に携るpixiv.netでは、オブジェクトはほとんど利用しません。