[PHP]__clone()とは?オブジェクトの複製時の挙動を制御する方法を徹底解説

PHP

はじめに

これまでの記事で、オブジェクトを関数のように扱う __invoke() や、コードとしての復元を担う __set_state() を解説してきました。今回取り上げる __clone() は、clone キーワードによってオブジェクトが複製される際に自動的に呼び出されるマジックメソッドです。

PHPでは、clone $obj という構文を使うことで、既存のオブジェクトのコピーを新しく作成できます。しかし、デフォルトの複製処理は「浅いコピー(shallow copy)」であり、オブジェクト内部に別のオブジェクトへの参照が含まれている場合、その参照先までは複製されず、複製後も元のオブジェクトと複製されたオブジェクトが同じ内部オブジェクトを共有してしまうという問題が起こります。__clone() は、この複製処理に独自のロジックを割り込ませ、必要な部分を「深いコピー(deep copy)」にするなど、挙動を細かく制御するために使われます。本記事では基本的な使い方から実践的な活用例まで詳しく解説します。


メソッド概要

項目内容
メソッド名__clone()
種別マジックメソッド
シグネチャpublic function __clone(): void
引数なし
呼び出しタイミングclone $obj によってオブジェクトが複製された直後
戻り値なし(void)
対応バージョンPHP 5.0以降
デフォルトの挙動__clone()がなくてもcloneは動作するが、浅いコピーになる

浅いコピーと深いコピーの違い(イメージ図)

  class Engine { public string $type = 'V6'; }
  class Car {
      public Engine $engine;
      public function __construct() { $this->engine = new Engine(); }
  }

  $carA = new Car();
  $carB = clone $carA;
              │
              ▼
  ┌───────────────────────────┐
  │ 【__clone()を実装しない場合(浅いコピー)】   │
  │ $carA->engine と $carB->engine は          │
  │ 同じEngineオブジェクトを指している!           │
  │ → $carB->engine->type を変更すると           │
  │   $carA->engine->type も変わってしまう        │
  └───────────────────────────┘

  ┌───────────────────────────┐
  │ 【__clone()を実装した場合(深いコピー)】     │
  │ public function __clone(): void {           │
  │     $this->engine = clone $this->engine;     │
  │ }                                            │
  │ ★この記事の対象                             │
  │ → $carBは独立した新しいEngineオブジェクトを持つ │
  └───────────────────────────┘

ポイントは、clone キーワード自体はトップレベルのプロパティをコピーしますが、プロパティがオブジェクトへの参照である場合、その参照先までは自動的に複製されないという点です。__clone() の中で、こうした内部オブジェクトを明示的に clone し直すことで、本当の意味で独立した複製を作ることができます。


実践サンプル7選

例1:基本的な使い方(浅いコピーの問題を確認)

<?php

class Engine
{
    public string $type = 'V6';
}

class Car
{
    public Engine $engine;

    public function __construct()
    {
        $this->engine = new Engine();
    }
}

$carA = new Car();
$carB = clone $carA; // __clone()が未定義なので浅いコピーになる

$carB->engine->type = 'V8';
echo $carA->engine->type . PHP_EOL; // "V8" になってしまう(意図しない共有)

例2:__clone()による深いコピーの実装

<?php

class Engine
{
    public string $type = 'V6';
}

class Car
{
    public Engine $engine;

    public function __construct()
    {
        $this->engine = new Engine();
    }

    /**
     * 複製時に、内部のEngineオブジェクトも
     * 明示的に複製し直すことで独立性を保つ
     */
    public function __clone(): void
    {
        $this->engine = clone $this->engine;
    }
}

$carA = new Car();
$carB = clone $carA;

$carB->engine->type = 'V8';
echo $carA->engine->type . PHP_EOL; // "V6" のまま(独立している)
echo $carB->engine->type . PHP_EOL; // "V8"

例3:複製時に一意なIDを再生成するクラス

<?php

class Document
{
    public string $id;

    public function __construct(public string $title)
    {
        $this->id = uniqid('doc_');
    }

    /**
     * 複製されたオブジェクトには、
     * 元とは異なる新しいIDを割り当てる
     */
    public function __clone(): void
    {
        $this->id = uniqid('doc_');
    }
}

$original = new Document('サンプル文書');
$copy = clone $original;

echo "{$original->id} vs {$copy->id}" . PHP_EOL; // それぞれ異なるIDになる

例4:複数階層にネストしたオブジェクトの複製

<?php

class Wheel
{
    public function __construct(public string $brand)
    {
    }
}

class Chassis
{
    /** @var Wheel[] */
    public array $wheels;

    public function __construct()
    {
        $this->wheels = [new Wheel('A社'), new Wheel('A社'), new Wheel('A社'), new Wheel('A社')];
    }

    public function __clone(): void
    {
        // 配列内のオブジェクトもそれぞれ複製する
        $this->wheels = array_map(fn (Wheel $w) => clone $w, $this->wheels);
    }
}

class Vehicle
{
    public Chassis $chassis;

    public function __construct()
    {
        $this->chassis = new Chassis();
    }

    /**
     * __clone()内で別のオブジェクトをcloneすると、
     * そのオブジェクトの__clone()も連鎖的に呼ばれる
     */
    public function __clone(): void
    {
        $this->chassis = clone $this->chassis;
    }
}

$v1 = new Vehicle();
$v2 = clone $v1;
$v2->chassis->wheels[0]->brand = 'B社';

echo $v1->chassis->wheels[0]->brand . PHP_EOL; // "A社" のまま
echo $v2->chassis->wheels[0]->brand . PHP_EOL; // "B社"

例5:複製を禁止する設計(Singletonパターンでの活用)

<?php

class AppSettings
{
    private static ?self $instance = null;
    public array $values = [];

    private function __construct()
    {
    }

    public static function getInstance(): self
    {
        self::$instance ??= new self();
        return self::$instance;
    }

    /**
     * 唯一のインスタンスであることを保証するため、
     * クローンの試み自体を例外で禁止する
     */
    public function __clone(): void
    {
        throw new LogicException('このオブジェクトは複製できません(Singleton)');
    }
}

$settings = AppSettings::getInstance();

try {
    $copy = clone $settings;
} catch (LogicException $e) {
    echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}

例6:イミュータブルなオブジェクトを複製してwith系メソッドを実装する

<?php

final class Point
{
    public function __construct(
        public readonly float $x,
        public readonly float $y
    ) {
    }

    /**
     * readonlyプロパティはclone内では直接変更できないため、
     * 新しいインスタンスを生成して返す「with」パターンを使う
     * (この場合__clone()自体は不要だが、比較のために掲載)
     */
    public function withX(float $x): self
    {
        return new self($x, $this->y);
    }
}

$p1 = new Point(1.0, 2.0);
$p2 = $p1->withX(5.0);
echo "({$p1->x}, {$p1->y}) / ({$p2->x}, {$p2->y})" . PHP_EOL;

例7:複製時にログを記録して追跡するクラス

<?php

class TrackedEntity
{
    private static int $cloneCount = 0;

    public function __construct(public string $name)
    {
    }

    /**
     * オブジェクトが複製されるたびに、
     * デバッグ・監査目的でカウントやログを記録する
     */
    public function __clone(): void
    {
        self::$cloneCount++;
        echo "{$this->name} が複製されました(累計: " . self::$cloneCount . "回)" . PHP_EOL;
    }

    public static function getCloneCount(): int
    {
        return self::$cloneCount;
    }
}

$original = new TrackedEntity('テストエンティティ');
$copy1 = clone $original;
$copy2 = clone $copy1;
echo 'クローン総数: ' . TrackedEntity::getCloneCount() . PHP_EOL;

関連機能との比較

機能役割__clone()との違い
__clone()cloneによるオブジェクト複製時の挙動を制御本記事の対象。複製直後に自動的に呼ばれる
cloneキーワードオブジェクトの複製を実行する言語構文__clone()を呼び出す側のトリガーとなる操作
__construct()通常のオブジェクト生成時に呼ばれるcloneでは呼ばれない点が重要な違い
readonlyプロパティ(PHP 8.1以降)初期化後の変更を禁止する__clone()内での変更に一部制約があるため、設計次第では「with」パターンとの併用が必要
serialize()/unserialize()オブジェクトの完全な複製(深いコピー)の代替手段より重い処理だが、ネストしたすべてのオブジェクトを一括で深くコピーできる

よくある落とし穴(注意点)

  1. __clone()を実装しないとデフォルトで浅いコピーになる これが最も重要な注意点です。オブジェクト内に他のオブジェクトへの参照が含まれる場合、__clone() を実装しないと、複製後も内部オブジェクトが元のオブジェクトと共有されたままになります(例1・例2を参照)。
  2. 複製時に__construct()は呼ばれない clone によるオブジェクト生成では、通常のコンストラクタは実行されません。コンストラクタでのみ行っていた初期化処理が複製後にも必要な場合、__clone() 内で改めて実装する必要があります。
  3. readonlyプロパティの扱いに注意する PHP 8.1以降のreadonlyプロパティは、一度初期化されると通常は変更できませんが、__clone() メソッド内に限り、未初期化状態にリセットしてから再設定するような特別な操作が可能な場合があります(PHPのバージョンによる挙動の違いに注意が必要です)。シンプルな設計を優先する場合は、例6のような「with」パターンの採用も検討しましょう。
  4. ネストが深い場合、すべての階層で__clone()を実装する必要がある オブジェクトが何重にもネストしている場合、深いコピーを実現するには、各階層のクラスすべてで __clone() を適切に実装する必要があります(例4を参照)。1箇所でも実装を忘れると、その階層だけ浅いコピーのままになってしまいます。
  5. 配列内のオブジェクトは自動的には複製されない __clone() 自体はトップレベルのプロパティに対して自動的には何もしないため、配列プロパティの中にオブジェクトが含まれる場合、array_map() などを使って明示的に各要素を複製する処理が必要です(例4を参照)。

まとめ

観点まとめ
何をするメソッドかcloneキーワードによるオブジェクト複製の直後に、追加の処理を行う
主な用途内部オブジェクトの深いコピー、一意なIDの再生成、複製の禁止(Singleton)、監査ログの記録
デフォルトの挙動__clone()がなくても複製は可能だが、内部オブジェクトは浅いコピーになる
呼ばれないメソッド複製時に__construct()は呼ばれない
注意点ネストしたオブジェクトでの各階層での実装、配列内オブジェクトの明示的な複製、readonlyプロパティとの兼ね合い

__clone() は、オブジェクトの複製における「浅いコピー」と「深いコピー」の違いを正しく制御するために欠かせないマジックメソッドです。内部に他のオブジェクトへの参照を持つクラスを設計する際は、意図しない状態共有のバグを防ぐためにも、__clone() の実装を忘れずに検討しましょう。

タイトルとURLをコピーしました