سرفصل‌های آموزشی
آموزش OOP در PHP
آشنایی با نحوهٔ Clone کردن یک آبجکت در زبان PHP

آشنایی با نحوهٔ Clone کردن یک آبجکت در زبان PHP

Object Cloning به پروسهٔ کپی کردن یک آبجکت در زبان‌های برنامه‌نویسی شیئ‌گرا گفته می‌شود و این در حالی است که در زبان پی‌اچ‌پی دو مُدل آبجکت کلونینگ داریم که عبارتند از Shallow Copy و Deep Copy که در ادامهٔ آموزش هر دو مورد را مورد بررسی قرار خواهیم داد.

پیش از هر چیز، داخل پوشهٔ oop پروژهٔ جدیدی به نام object-cloning ساخته سپس بر پایهٔ ساختاری که در این دوره تاکنون دنبال نموده‌ایم، در مسیر روت فایلی به نام index.php ساخته و آن را به صورت زیر تکمیل می‌کنیم:

<?php
$varOne = 1;
$varTwo = $varOne;
++$varTwo;
echo $varOne;
echo "\n";
echo $varTwo;

همان‌طور که می‌بینیم، varOne$ حاوی عدد ۱ است و در خط بعد متغیر دیگری ساخته‌ایم به نام varTwo$ که مقدار آن را برابر با varOne$ قرار داده‌ایم و در ادامه مقدار متغیر varTwo$ به اصطلاح Increment کرده‌ایم (یک واحد افزایش داده‌ایم.) و در نهایت هر دو متغیر را چاپ کرده‌ایم به طوری که در خروجی خواهیم داشت:

1
2

می‌بینیم که درج علائم ++ قبل از varTwo$ هیچ تأثیری روی مقدار varOne$ ندارد زیرا وقتی این متغیر را کپی کردیم،‌ اصطلاحاً‌ Pass by Value بود بدان معنا که فضای جدیدی در حافظه ایجاد شده و مقدار متغیر varTwo$ در آن ذخیره شد و از این پس این دو متغیر هیچ ربطی به یکدیگر نخواهند داشت. در ادامه، قصد داریم تا با مفهومی تحت عنوان Pass by Reference آشنا شویم به طوری که داریم:

<?php
$varOne = 1;
$varTwo = &$varOne;
++$varTwo;
echo $varOne;
echo "\n";
echo $varTwo;

در حقیقت، در خط سوم قبل از متغیر varOne$ از علامت & استفاده کرده‌ایم به طوری که می‌توان گفت از این پس متغیر varOne$ یک پوینتر یا رِفرنس به جایی از حافظه است که مقدار این متغیر در آن ذخیره شده است و اگر این فایل را اجرا کنیم،‌ در خروجی خواهیم داشت:

2
2

وقتی از علائم ++ قبل از متغیر varTwo$ استفاده کنیم، یک واحد به مقدار این متغیر افزوده می‌شود اما از آنجا که متغیر varOne$ به اصطلاح Pass by Reference است و اساساً متغیر varTwo$ یک رفرنس به جایگاهی از حافظه است که متغیر varOne$ در آن ذخیره شده است، می‌توان گفت که در حافظه فقط و فقط یک دیتا ثبت شده که هم متغیر varOne$ و هم متغیر varTwo$ به آن فضا اشاره دارند و نیاز به توضیح نیست که وقتی مقدار یکی از متغیرهای تغییر کند،‌ مقدار دیگری هم تغییر پیدا خواهد کرد چرا که هر دو به فضایی یکسان از حافظه اشاره دارند (در همین راستا و برای کسب اطلاعات بیشتر می‌توانید به مقالهٔ آشنایی با تفاوت Pass by Value و Pass By Reference در زبان PHP مراجعه نمایید.)

حال به منظور آشنایی با نحوهٔ‌ کپی کردن آبجکت‌ها،‌ داخل پوشهٔ classes فایلی به نام User.php ساخته و آن را به صورت زیر تکمیل می‌کنیم:

<?php
namespace SokanAcademy;

class User
{
    public $projectName;

    public function __construct($project)
    {
        $this->projectName = $project;
    }
}

داخل بدنهٔ این کلاس یک پراپرتی داریم به نام projectName$ که به محض ساخت یک آبجکت از روی این کلاس، می‌باید پارامتری تحت عنوان project$ به کانستراکتور این کلاس پاس دهیم تا در نهایت به این پراپرتی منتسب گردد. حال وارد فایل index.php شده و آن را به صورت زیر تکمیل می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$user = new SokanAcademy\User('Sokan Academy');
echo $user->projectName;

از روی کلاس User با در نظر گرفتن استرینگی به عنوان آرگومان این کلاس، آبجکتی ساخته‌ایم به نام user$ سپس در خط بعد تنها پراپرتی این کلاس را چاپ کرده‌ایم به طوری که در خروجی خواهیم داشت:

Sokan Academy

حال این فایل را به صورت زیر آپدیت می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$user = new SokanAcademy\User('Sokan Academy');
echo $user->projectName;
echo "\n";
$newUser = $user;
$newUser->projectName = 'New Project';
echo $newUser->projectName;
echo "\n";
echo $user->projectName;

در خط هشتم یک آبجکت جدید ساخته‌ایم تحت عنوان newUser$ اما به جای آن که با استفاده از کلیدواژهٔ new نمونهٔ جدیدی از روی کلاس User ساخته و به آن منتسب کنیم، از آبجکت قبلی user$ استفاده کرده‌ایم. در ادامه، مقدار جدیدی برای پراپرتی projectName$ منتسب به این آبجکت جدید در نظر گرفته و در خط بعد آن را چاپ کرد‌ه‌ایم و در نهایت مجدد پراپرتی projectName$ منتسب به آبجکت اصلی یا همان user$ را چاپ کرده‌ایم به طوری که در خروجی خواهیم داشت:

Sokan Academy
New Project
New Project

در تفسیر خروجی فوق باید گفت که چاپ استرینگ «Sokan Academy» مربوط به چاپ پراپرتی projectName$ در خط ششم است و استرینگ «New Project» اول مربوط به اجرای خط دهم است و استرینگ «New Project» دوم مربوط به اجرای خط دوازدهم می‌باشد. همان‌طور که می‌بینیم، پس از ساخت یک آبجکت جدید و تغییر مقدار تنها پراپرتی آن، مقدار پراپرتی موجود در آبجکت اول که نقش آبجکت اصلی را بازی می‌کند نیز تغییر یافته است!

این مسئله از آنجا ناشی می‌شود که ما اساساً آبجکتی را کپی نکرده‌ایم بلکه متغیری ساخته‌ایم تحت عنوان newUser$ که به منزلهٔ یک پوینتر (اشاره‌گر) به دقیقاً همان بخش از حافظه می‌باشد که متغیر user$ در آن ذخیره شده است و بالتبع وقتی هر کدام از این پوینترها منجر به تغییر دیتا شوند، داده‌های هر دو متغیر آپدیت خواهند شد. برای درک بهتر این موضوع، فایل index.php را به صورت زیر مجدد تغییر می‌دهیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$user = new SokanAcademy\User('Sokan Academy');
echo $user->projectName;
echo "\n";
$newUser = $user;
$newUser->projectName = 'New Project';
echo $newUser->projectName;
echo "\n";
echo $user->projectName;
echo "\n";
$user->projectName = 'Back to Sokan Academy';
echo $user->projectName;
echo "\n";
echo $newUser->projectName;

همان‌طور که ملاحظه می‌شود، در خط چهاردهم مجدد مقدار جدیدی برای پراپرتی منتسب به آبجکت user$ در نظر گرفته‌ایم و در خط بعد آن را چاپ نموده‌ایم و در خط آخر مجدد پراپرتی منتسب به آبجکت جدید را چاپ کرده‌ایم به طوری که داریم:

Sokan Academy
New Project
New Project
Back to Sokan Academy
Back to Sokan Academy

می‌بینیم که مجدد استرینگ جدید «Back to Sokan Academy» که به پراپرتی آبجکت اصلی اختصاص یافته است برای پراپرتی آبجکت کپی نیز در نظر گرفته شده است. حال برای این که از بُعد دیگری یکسان بودن هر دو آبجکت را چک کنیم، کدهای فوق را به صورت زیر تکمیل می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$user = new SokanAcademy\User('Sokan Academy');
echo $user->projectName;
echo "\n";
$newUser = $user;
$newUser->projectName = 'New Project';
echo $newUser->projectName;
echo "\n";
echo $user->projectName;
echo "\n";
$user->projectName = 'Back to Sokan Academy';
echo $user->projectName;
echo "\n";
echo $newUser->projectName;
echo "\n";
var_dump(spl_object_hash($user));
var_dump(spl_object_hash($newUser));

با اجرای مجدد این فایل نیز خواهیم داشت:

Sokan Academy
New Project
New Project
Back to Sokan Academy
Back to Sokan Academy
string(32) "00000000096d82ac000000006e7fdb05"
string(32) "00000000096d82ac000000006e7fdb05"

در واقع،‌ فانکشن ()spl_object_hash شناسهٔ به اصطلاح Hash ID هر آبجکت را باز می‌گرداند و همان‌طور که می‌بینیم، مقدار هَش هر دو آبجکت دقیقاً یکسان است چرا که هر دو آبجکت به جایگاهی یکسان از حافظه ارجاع می‌دهند. در حقیقت،‌ می‌بینیم که در زبان پی‌اچ‌پی وقتی با آبجکت‌ها سروکار داریم، گویی کپی کردن بدین شکل Pass By Reference است. با این تفاسیر، ایدهٔ این طور کلون کردن آبجکت‌ها کارساز نیست و می‌باید به دنبال راه‌کار دیگری باشیم که پروسهٔ Object Cloning به شکل اصولی‌تری صورت گیرد که در ادامهٔ این آموزش به بررسی روش‌های دیگر انجام این کار خواهیم پرداخت.

آشنایی با کلیدواژهٔ‌ clone به منظور کپی کردن آبجکت‌ها

به منظور ایجاد یک به اصطلاح Shallow Copy از یک آبجکت،‌ دستوری در هستهٔ‌ زبان پی‌اچ‌پی گنجانده شده تحت عنوان clone که این امکان را در اختیار توسعه‌دهنده قرار می‌دهد تا از روی یک آبجکت به‌خصوص کپی گیرد. به منظور درک سازوکار این دستور،‌ کدهای نوشته‌شده داخل فایل index.php را به صورت زیر تغییر می‌دهیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$user = new SokanAcademy\User('Sokan Academy');
echo $user->projectName;
echo "\n";
$newUser = clone $user;
$newUser->projectName = 'New Project';
echo $newUser->projectName;
echo "\n";
echo $user->projectName;
echo "\n";
$user->projectName = 'Back to Sokan Academy';
echo $user->projectName;
echo "\n";
echo $newUser->projectName;
echo "\n";
var_dump(spl_object_hash($user));
var_dump(spl_object_hash($newUser));

همان‌طور که ملاحظه می‌شود، در خط هشتم پس از علامت = از دستور clone استفاده کرده‌ایم به طوری که اگر از این پس فایل فوق را اجرا کنیم، در خروجی خواهیم داشت:

Sokan Academy
New Project
Sokan Academy
Back to Sokan Academy
New Project
string(32) "0000000052857a11000000000bb1788c"
string(32) "0000000052857a10000000000bb1788c"

می‌بینیم که این بار تداخلی مابین پراپرتی‌های هر دو آبجکت پیش نمی‌آید مضاف بر این که می‌توان گفت که خروجی فانکشن ()spl_object_hash این بار متفاوت است به طوری که هَش مربوط به آبجکت user$ متفاوت از هَش مربوط به آبجکت newUser$ است و اساساً به همین دلیل هم می‌باشد که ایجاد تغییر در پراپرتی یک آبجکت روی آبجکت‌ دیگر تداخلی ایجاد نمی‌کند.  در عین حال باید توجه داشته باشیم که رفتار دستور clone زمانی که پارامتر ورودی کلاس User یک آبجکت دیگر باشد کاملاً متفاوت خواهد بود که برای روشن‌تر شدن این موضوع، کلاس User را به صورت زیر تغییر می‌دهیم:

<?php
namespace SokanAcademy;

class User
{
    public $projectName;
    
    public function __construct(Project $project)
    {
        $this->projectName = $project;
    }
}

در واقع،‌ بر اساس قوانین Type Hinting دستور داده‌ایم که پارامتر ورودی کانستراکتور این کلاس حتماً‌ می‌باید آبجکتی از جنسِ کلاس Project باشد که در ادامه و داخل پوشهٔ‌ classes فایلی می‌سازیم تحت عنوان Project.php و چنین کلاسی داخل آن می‌سازیم:

<?php
namespace SokanAcademy;

class Project
{
    public $name;

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

ساختار این کلاس تا حد بسیاری شبیه به کلاس User است و از همین روی زمان زیادی روی تفسیر آن صرف نمی‌کنیم و بلافاصله وارد فایل index.php شده و آن را به صورت زیر آپدیت می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$project = new SokanAcademy\Project('Sokan Academy');
$user = new SokanAcademy\User($project);

$newUser = clone $user;

var_dump(spl_object_hash($user));
var_dump(spl_object_hash($newUser));

echo "\n";

var_dump(spl_object_hash($user->projectName));
var_dump(spl_object_hash($newUser->projectName));

echo "\n";

$user->projectName->name = 'New Project Name';

var_dump($user->projectName->name);
var_dump($newUser->projectName->name);

ابتدا خروجی را مشاهده کرده سپس به تفسیر کدهای جدید خواهیم پرداخت:

string(32) "0000000054e0ffce0000000017bdf5ff"
string(32) "0000000054e0ffc80000000017bdf5ff"

string(32) "0000000054e0ffcf0000000017bdf5ff"
string(32) "0000000054e0ffcf0000000017bdf5ff"

string(16) "New Project Name"
string(16) "New Project Name"

در واقع، دو استرینگ اول در خروجی مربوط به هَش‌های آبجکت‌های user$ و newUser$ هستند و می‌بینیم که متفاوت می‌باشند و دو هَش دوم مربوط به شناسهٔ پراپرتی projectName$ در کلاس User می‌باشند که به شکل عجیبی می‌بینیم برای هر دو آبجکت یکسان است! به عبارتی، اگر مقدار پراپرتی name$ در کلاس Project را همان‌طور که در خط بیستم می‌بینیم دستخوش تغییر سازیم، این مقدار در هر دو آبجکت آپدیت خواهد شد.  در حقیقت، هم user$ و هم newUser$ آبجکت‌های منحصر‌به‌فردی هستند اما پراپرتی‌های آن‌ها یکسان است و به یک نقطه از حافظه اشاره دارند زیرا مفسر پی‌اچ‌پی دست به بازآفرینی آدرس حافظهٔ آبجکت‌هایی نمی‌زند که به عنوان پارامتر ورودی دیگر آبجکت‌ها مورد استفاده قرار می‌گیرند که به این نوع کلون کردن آبجکت‌ها Shallow Copy گفته می‌شود و اگر قصد داریم تا دو آبجکت به معنای واقعی کلمه متمایز داشته باشیم،‌ می‌باید با مفهومی تحت عنوان Deep Copy آشنا شویم که این کار با اِعمال مَجیک متد ()clone__ امکان‌پذیر است. برای این منظور، کلاس User را به صورت زیر تغییر می‌دهیم:

<?php
namespace SokanAcademy;

class User
{
    public $projectName;

    public function __clone()
    {
        $this->projectName = clone $this->projectName;
    }
    
    public function __construct(Project $project)
    {
        $this->projectName = $project;
    }
}

در حقیقت،‌ وقتی که مفسر پی‌اچ‌پی یک آبجکت را کلون می‌کند، مَجیک متد ()clone__ برای آبجکت جدید فراخوانی می‌گردد و بالتبع مفسر دست به بازآفرینی آدرس حافظه برای پراپرتی projectName$ خواهد زد و اگر مجدد به فایل index.php بازگشته و آن را اجرا کنیم، در خروجی خواهیم داشت:

string(32) "000000003a26139c0000000012ab8d26"
string(32) "000000003a26139a0000000012ab8d26"

string(32) "000000003a26139d0000000012ab8d26"
string(32) "000000003a26139b0000000012ab8d26"

string(16) "New Project Name"
string(13) "Sokan Academy"

می‌بینیم خط بیست‌وسوم که مسئول چاپ مقدار پراپرتی مرتبط با آبجکت کلون‌شده است، علیرغم اِعمال تغییر در پراپرتی آبجکت اصلی، بدون تغییر باقی مانده است که این کار اصطلاحاً‌ Deep Copy نامیده می‌شود.

جمع‌بندی
در این آموزش به بررسی مقولهٔ Object Cloning یا به عبارتی کپی کردن آبجکت‌ها در زبان پی‌اچ‌پی پرداختیم و دیدیم که به چه شکل با استفاده از دستور clone و مَجید متد ()clone__ می‌توان مفاهیم Shallow Copy و Deep Copy را در این زبان پیاده‌سازی نمود. در پایان لازم به یادآوری است که به منظور پیاده‌سازی Deep Copy در ارتباط با آبجکت‌ها، می‌توان از لایبرری اپن‌سورس DeepCopy نیز استفاده نمود.

online-support-icon