PHPでのデータのコピーについて

 

はじめに

 データをカプセル化する場合、getterではデータのコピーを返すことが推奨されます。データそのものではなくコピーを返すことで、データの変更を、ラップしたクラスのメソッドからのみに制限することができます。これにより予期しないデータの変更を防ぎ、コードの安定性と信頼性を向上させることが出来ます。

 PHPでデータのコピーを行う方法に以下の3つがあります。
  1.`clone`キーワードを使う
  2.配列関数を使う
  3.unserialize/serialize関数を使う

 それぞれの使用方法を紹介します。また、どれを使うべきか結論を先に述べると、最も効果的な方法は3番目のunserialize/serialize関数を使う方法になります。

 

1.`clone`キーワードを使う方法

 `clone`キーワードはオブジェクトのクローンを作成します。クローンは、オブジェクトプロパティの値のシャローコピーを行います。これにより、コピー先とコピー元のオブジェクトは同じ参照を共有し、変更が相互に影響を与えることがあります。プロパティごとコピーしたい場合は、`__clone()`メソッドをオーバーライドしてその中でさらにプロパティを`clone`する必要があります。また、`clone`キーワードはオブジェクトのみに有効であり、配列では使用できません。

  例
 名前とメールアドレスをプロパティに持つオブジェクトをコピーします。コピー先のメールアドレスの変更はコピー元に影響を与えません。

class Person
{
    public string $name;
    public string $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
}

$original = new Person('山田 太郎', 'taro_yamada@example.com');
$copy = clone $original;
$copy->email = 'taro_yamada@hoge.co.jp';

echo $original->email; // taro_yamada@example.com
echo $copy->email;     // taro_yamada@hoge.co.jp

 オブジェクトがプロパティに参照を保持する場合は注意が必要です。先程のPersonクラスのプロパティに会社を追加しました。そのまま`clone`を行うと参照がコピーされてしまい、コピー先の変更がコピー元に影響を及ぼすため、`__clone()`メソッドをオーバーライドし、ネストして`clone`する必要があります。

class Person
{
    public string $name;
    public string $email;
    public Company $company;

    public function __construct($name, $email, $company)
    {
        $this->name = $name;
        $this->email = $email;
        $this->company = $company;
    }

    public function __clone()
    {
        // ネストしてプロパティを`clone`する必要がある
        $this->company = clone $this->company;
    }
}

class Company
{
    public $name;
    public $address;

    public function __construct($name, $address)
    {
        $this->name = $name;
        $this->address = $address;
    }
}

$original = new Person(
    '山田 太郎',
    'taro_yamada@example.com',
    new Company('株式会社〇〇', '東京都千代田区千代田1-1')
);
$copy = clone $original;
$copy->company->address = '福岡県福岡市博多区博多1-1';

echo $original->company->address; // 東京都千代田区千代田1-1
echo $copy->company->address;     // 福岡県福岡市博多区博多1-1

 また、配列をプロパティとして持つ場合は、配列には`clone`を使用できないため、後述する2か3の方法を使用する必要があります。

class Person
{
    public string $name;
    public array $courses;

    public function __construct($name, $courses)
    {
        $this->name = $name;
        $this->courses = $courses;
    }

    public function __clone()
    {
        // `clone`は配列に使用できない
        $this->courses = array_slice($this->courses, 0);
    }

    public function numAdvancedCourses(): int
    {
        return count(
            array_filter(
                $this->courses,
                fn($course) => $course['is_advanced']
            )
        );
    }
}

$original = new Person(
    '山田 太郎',
    [
        ['name' => 'basic1', 'is_advanced' => false],
        ['name' => 'basic2', 'is_advanced' => false],
        ['name' => 'basic3', 'is_advanced' => false]
    ]
);
$copy = clone $original;
$copy->courses[0]['is_advanced'] = true;

echo $original->numAdvancedCourses(); // 0
echo $copy->numAdvancedCourses();     // 1

 

2.配列関数を使う方法

 PHPの配列関数を使用して配列のコピーを作成することができます。`array_slice()`や`array_merge()`などの関数は戻り値として新しい配列を返すため、元の配列に影響を与えることなくコピーを作成できます。オブジェクトの配列をコピーする場合、配列関数は参照をコピーするため、オブジェクトの変更が元の配列内のオブジェクトにも影響を与えることがあります。これを防ぐために、要素ごとに個別にコピーを作成する必要があります。

  例
 `array_slice()`、`array_filter()`、`array_map()`を使用して連想配列をコピーします。それぞれの配列関数では新しい配列を返すため、元の変数への影響の気にせずにコピー先の配列を変更することができます。

$original = [
    'first_name' => '太郎',
    'family_name' => '山田',
    'email' => 'taro_yamada@example.com'
];

$copy_1 = array_slice($original, 0);
$copy_1['family_name'] = '田中';

$copy_2 = array_filter($copy_1, fn() => true);
$copy_2['family_name'] = '佐藤';

$copy_3 = array_map(fn($person) => $person, $copy_2);
$copy_3['family_name'] = '鈴木';

echo $original['family_name'];  // 山田
echo $copy_1['family_name'];    // 田中
echo $copy_2['family_name'];    // 佐藤
echo $copy_3['family_name'];    // 鈴木

 オブジェクトの配列の場合は注意が必要です。`array_map()`を使用してオブジェクトごとに個別にコピーを作成する必要があります。

class Course {
    public string $name;
    public bool $is_advanced;

    public function __construct($name, $is_advanced) {
        $this->name = $name;
        $this->is_advanced = $is_advanced;
    }
}

$original = [
    new Course('basic1', false),
    new Course('basic2', false),
    new Course('basic3', false)
];

// オブジェクトごとに個別にコピーする
$copy = array_map(fn($course) => clone $course, $original);
$copy[0]->is_advanced = true;

echo $original[0]->is_advanced ? 'true' : 'false';  // false
echo $copy[0]->is_advanced ? 'true' : 'false';      // true

 

3.unserialize/serialize関数を使う方法

 PHPでは`unserialize()`と`serialize()`を使用することでデータのディープコピーを作成することができます。この方法はオブジェクトや配列をシリアライズしてからアンシリアライズすることで、元のデータの完全なコピーを作成します。これにより、ネストされたオブジェクトや参照を含むデータ構造の場合でも正確なコピーを作成することができます。

  例
 1の例でこの方法を使用してみます。

class Person
{
    public string $name;
    public string $email;
    public Company $company;

    public function __construct($name, $email, $company)
    {
        $this->name = $name;
        $this->email = $email;
        $this->company = $company;
    }
}

class Company
{
    public string $name;
    public string $address;

    public function __construct($name, $address)
    {
        $this->name = $name;
        $this->address = $address;
    }
}

$original = new Person(
    '山田 太郎',
    'taro_yamada@example.com',
    new Company('株式会社〇〇', '東京都千代田区千代田1-1')
);
// clone から unserialize(serialize()) に変更
$copy = unserialize(serialize($original)); 
$copy->company->address = '福岡県福岡市博多区博多1-1';

echo $original->company->address; // 東京都千代田区千代田1-1
echo $copy->company->address;     // 福岡県福岡市博多区博多1-1

 Personクラスから`__clone()`メソッドを削除し、コピーを`clone`から`unserialize(serialize())`に変更しました。これは1の時と同じ結果になります。

 2の例でも試してみます。

class Course {
    public string $name;
    public bool $is_advanced;

    public function __construct($name, $is_advanced) {
        $this->name = $name;
        $this->is_advanced = $is_advanced;
    }
}

$original = [
    new Course('basic1', false),
    new Course('basic2', false),
    new Course('basic3', false)
];

// array_map() から unserialize(serialize()) に変更
$copy = unserialize(serialize($original));
$copy[0]->is_advanced = true;

echo $original[0]->is_advanced ? 'true' : 'false';  // false
echo $copy[0]->is_advanced ? 'true' : 'false';      // true

 コピーを`array_map()`から`unserialize(serialize())`に変更しました。これは2の時と同じ結果になります。

 ネストされたオブジェクトや複雑なデータ構造の場合にも簡潔に対応することが出来ます。

class Person
{
    public string $name;
    public string $email;
    public Company $company;
    public array $courses;

    public function __construct($name, $email, $company, $courses)
    {
        $this->name = $name;
        $this->email = $email;
        $this->company = $company;
        $this->courses = $courses;
    }
}

class Company
{
    public string $name;
    public string $address;

    public function __construct($name, $address)
    {
        $this->name = $name;
        $this->address = $address;
    }
}

class Course {
    public string $name;
    public bool $is_advanced;

    public function __construct($name, $is_advanced) {
        $this->name = $name;
        $this->is_advanced = $is_advanced;
    }
}

$original = new Person(
    '山田 太郎',
    'taro_yamada@example.com',
    new Company('株式会社〇〇', '東京都千代田区千代田1-1'),
    [
        new Course('basic1', false),
        new Course('basic2', false),
        new Course('basic3', false)
    ]
);
$copy = unserialize(serialize($original)); 
$copy->company->address = '福岡県福岡市博多区博多1-1';
$copy->courses[0]->is_advanced = true;

echo $original->company->address; // 東京都千代田区千代田1-1
echo $copy->company->address;     // 福岡県福岡市博多区博多1-1
echo $original->courses[0]->is_advanced ? 'true' : 'false'; // false
echo $copy->courses[0]->is_advanced ? 'true' : 'false';     // true

 

まとめ

 PHPにおいてデータのコピーを行う方法にはいくつかの選択肢があり、それぞれの方法には一長一短があります。

 ・`clone`キーワードはオブジェクトのプロパティの値のシャローコピーを作成するため、オブジェクトが参照を持つプロパティを含む場合には注意が必要です。この方法はシンプルですが、ネストされたオブジェクトのコピーには向いておらず、配列には使用できません。
 ・配列関数(`array_slice()`、`array_filter()`、`array_map()`など)を使用する方法は、配列のコピーに適しています。しかし、オブジェクトのコピーを作成する際には、各オブジェクトの参照がコピーされるため、個別にオブエクトをコピーする必要があります。

 これらの方法に対して、unserialize/serialize関数を使用する方法は以下の点で優れています。
  1.汎用性の高さ
 この方法はあらゆるデータ型に対して適用可能です。配列やオブジェクトに限らず、様々なデータ構造に対しても利用できます。
  2.ディープコピーのサポート
 この方法はオブジェクトや配列のディープコピーを作成するため、複雑なデータ構造やネストされたオブジェクトにも対応できます。コピー先と元のデータが完全に独立するため、一報を変更しても他方に影響を与えることがありません。
  3.シンプルな実装
 `unserialize()`と`serialize()`を使用することで、ディープコピーのロジックを簡潔に実装出来ます。特に複雑なデータ構造を扱う場合、この方法が最も簡単で信頼性の高い方法になります。

 PHPでデータのコピーを行う際には、unserialize/serialize関数を使用する方法が最適です。開発の安定性とコードの可読性を保ちつつ、確実にデータをコピーするために、この方法を採用することをお勧めします。

 

お問い合わせはこちら