اصول شی‌گرایی در PHP

برای یادگیری اصول شی‌گرایی باید برنامه‌نویسی رویه‌گرا به خوبی آموخته باشید. شی‌گرایی در برنامه‌نویسی (OOP) نقطه مقابل برنامه نویسی رویه‌گرا (Procedure) است و در کنار آن استفاده می‌شود. در برنامه‌نویسی رویه‌گرا با تعدادی تابع و ثابت و متغییر سروکار داریم، همین مفاهیم با قدرت بیشتری در شی‌گرایی نیز وجود دارند، در شی‌گرایی اولین چیز کلاس‌ها هستند، کلاس (class) مانند یک راهنما است که قاعده کلی را شامل می‌شوند، شی (object) نیز یک نمونه است که از روی نقشه یا راهنمای ما که کلاس است ساخته شده، هر کلاس دارای متدهایی است که در واقع روش تعریف آن‌ها مانند توابع در برنامه‌نویسی رویه‌گرا هستند و پراپرتی همان متغییرها در برنامه‌نویسی رویه‌گرا هستند. اصول شی‌گرایی

مزایای برنامه‌نویسی به روش شی‌گرا

توسعه و نگهداری ساده‌تر

در برنامه‌نویسی، زمانی که برنامه بزرگ شود، طبیعتا کدها زیاد می‌شود و توسعه و نگهداری آن‌ها سخت می‌شود، شی‌گرایی این مشکل را با اصل جلوگیری از دوباره‌نویسی کدها (ِDRY) حل می‌کند، کد ما بهینه‌تر و توسعه و نگهداری آن در آینده نیز ساده‌تر خواهد بود.

کارگروهی

کار گروهی با استفاده از شی‌گرایی به این صورت است که هر توسعه‌دهنده مسئول بخشی از برنامه است و تعدادی وظیفه (Task) به هر برنامه‌نویس اختصاص می‌یابد، و هر برنامه‌نویس می‌تواند کلاس مربوط به بخشی از برنامه را نوشته و از کلاس‌های نوشته شده توسط سایر برنامه‌نویسان استفاده کند.

وراثت

در برنامه‌نویسی شی‌گرا مفهوم دیگری به نام وراثت وجود دارد که کاربرد آن در جلوگیری از بازنویسی کدها است که قبلا اشاره کردیم که توسعه را ساده‌تر می‌کند، اگر کلاس (Class) را به نقشه ماشین و شی (Object) را به یک ماشین ساخته شده از روی آن نقشه تشبیه کنیم، رفتار ماشین مانند ترمز گرفتن را می‌توان به متد (Method) و ویژگی‌های ماشین مانند رنگ یا دارا بودن یک ویژگی را می‌توان به پراپرتی (Property) تشبیه نمود، برای دو نوع ماشین نیاز به دو نوع نقشه داریم ولی برای دو ماشین از یک خانواده با آپشن‌های متفاوت نیاز به یک کلاس و دو شی داریم که هر شی مربوط به یک ماشین است.

کپسوله‌سازی (Encapsulation)

مکانیزمی برای اتصال متدها و پراپرتی‌ها به هم است که باعث جلوگیری از تداخل و استفاده نامناسب از آن‌ها می‌شود.

مفاهیم اولیه در شی‌گرایی

کلاس

هماطور که گفته شد، کلاس را می‌توان معادل یک نقشه طراحی فرض کرد، کد زیر سینتکس یک کلاس را نمایش می‌دهد.

class Car {
}

توجه نمایید که بهتر است هر کلاس را داخل یک فایل ذخیره کنید، بسته به کاربرد کلاس‌ها، الگوهای متفاوتی در نامگذاری کلاس‌ها وجود دارد، مثلا در پوشه‌ای با نام class و همنام با نام کلاس قرار بگیرند یا در پوشه دیگری با پیشوند class.Name.php قرار بگیرند که Name نام کلاس است، ولی در هر حال مهم رعایت قاعده کلی برای نامگذاری در برنامه است.

شی

شی یک نمونه‌ای است که از روی کلاس ساخته می‌شود، نحوه ساختن شی بسیار ساده است:

$obj = new ClassName;

در کد بالا یک شی به نام obj از کلاس ClassName ساخته شده است.

متد

متد معادل تابع در برنامه‌نویسی رویه‌گرا است، در بدنه کلاس نوشته می‌شود و می‌توان داخل یا بیرون از کلاس آن را فراخونی نمود:

class Car {
    function getName() {
        echo 'BMW';
    }
}

$car = new Car;
$car->getName();

در مثال بالا، یک تابع در بدنه کلاس تعریف شده که نام ماشین را نمایش می‌دهد، در خط 7 می‌بینید که یک شی برای استفاده از کلاس با استفاده از کلمه کلیدی new ساخته شده است، سپس در خط 8 متد را فراخوانی شده که متد می‌تواند هر چند بار بخواهید فرخوانی شود و نیازی به ساخت شی جدید نیست، این یک نمونه از نتایج رعایت اصول شی‌گرایی است که از کدنویسی دوباره جلوگیری می‌کند. برای دسترسی به متدها و پراپرتی‌های داخل یک کلاس از طریق شی ساخته شده از آن کلاس، باید ابتدا نام شی را بنویسیم، سپس مانند خط 8 مثال بالا، تنها کافیست نام پراپرتی یا متد را بنویسیم.

پراپرتی

پراپرتی مانند متغییرها در برنامه‌نویسی رویه‌گرا هستند و کار آن نگهداری داده‌ها است، اگر نیاز به تغییر این داده‌ها باشد، می‌توانیم از متدها استفاده کنیم، در کد زیر این مورد را مشاهده می‌کنید:

class Car {
    $carName;
    function setName($name) {
        $this->carName = $name;
    }
    function getName() {
        echo 'My car is a ' . $this->carName . '<br>';
    }
}

$car = new Car;
$car->setName('BMW');
$car->getName();
$car->getName();
$car->setName('Lamborghini');
$car->getName();

مروری بر کدهای بالا: خط 1: یک کلاس با نام Car تعریف شده است خط 2: یک پراپرتی برای نگهداری نام ماشین تعریف شده است. (همانطور که گفتم وظیفه پراپرتی نگهداری داده‌ها است.) خط 3: یک متد به نام setName تعریف شده است که یک پارامتر می‌گیرد که نام ماشین در آن ذخیره شده است. خط 4: در این خط پراپرتی مقداردهی شده است. (در ادامه نحوه مقداردهی را توضیح خواهم داد.) خط 6: متدی برای نمایش نام ماشین به همراه متن دلخواه ما است. خط 11: یک شی از کلاس ایجاد شده است. خط 12 و 15: متد مقداردهی نام ماشین با را مقدار دلخواه ما فراخوانی شده است، دقت کنید که تا خط 15 که مقدار جدیدی به این پراپرتی بدهیم، آخرین مقدار در آن حفظ می‌شود. خط 13، 14 و 16: این خطوط با توجه به آخرین مقدار نام ماشین، متن دلخواه ما را برمی‌گردانند.

شبه متغییر this

در مثال قبل دیدیم که با استفاده از شبه متغییر this توانستیم پراپرتی را مقداردهی نماییم، دلیل استفاده از this این است که عمل‌ها حتی مقداردهی فقط می‌توانند داخل متدها نوشته شوند و خارج از متد غیر از ثابت و پراپرتی چیز دیگری وجود نخواهد داشت، البته داخل متد می‌تواند متغییرهایی تعریف شود، حالا برای اینکه مفسر PHP بتواند تشخیص دهد که باید چه متغییری را مقداردهی کند، اگر بخواهیم متغییری داخل متد را مقداردهی کنیم، مانند برنامه‌نویسی رویه‌گرا نام متغییر را می‌نویسیم و مقدار را بعد از مساوی به آن نسبت می‌دهیم:

$firstName = 'Mojtaba';

اما اگر بخواهیم یک پراپرتی در بدنه کلاس که طبیعتا خارج از بدنه متد است را مقداردهی کنیم باید از شبه متغییر this استفاده کنیم،سپس اشاره به آن پراپرتی -> و مقدار مورد نظرمان را قرار می‌دهیم:

$this -> firstName = 'Mojtaba';

دقت کنید که پراپرتی باید تعریف شده باشد و در هنگام استفاده از شبه متغییر this دیگری نیازی نیست تا از $ دیگری برای نام پراپرتی استفاده کنیم. برای استفاده از مقدار پراپرتی نیز به روشی مشابه می‌توان مقدار را به دست آورد:

echo $this -> firstName;

وراثت (Inheritance)

یکی از قابلیت‌های مفید شی‌گرایی وراثت است که باعث کاهش بسیار زیاد کدها در برنامه می‌شود. مثلا وقتی بخواهیم برنامه‌ای برای مدیریت نیروی انسانی و امور مالی یک شرکت یا سازمان بنویسیم، هر گروه شغلی را می‌توانیم در یک گروه قرار دهیم، همه گروه‌ها عضو گروه بزرگتری به نام گروه کارمندان هستند که می‌توانیم کلاسی برای کارمندان بسازیم که پراپرتی‌های آن قطعا برای نگهداری نام و نام خانوادگی و سن و میزان حقوق و سایر اطلاعات شخصی و کاری کارمندان است:

class Employee {
}

هر گروه شغلی می‌تواند یک کلاس داشته باشد که پراپرتی‌ها و متدهای منحصر به فردی داشته باشد، مثلا گروهی از کارمندان به ماموریت اعزام می‌شوند پس باید هزینه‌های اسکان، رفت و آمد و حق ماموریت برای آن‌ها در نظر گرفته شود. اگر این گروه متخصصان نام داشته باشند از کلاس کارمندان ارث‌بری می‌کنند تا از نوشتن دستورات تکراری جلوگیری شود که به کلاس متخصصان، کلاس فرزند (child) و به کلاس کارمندان کلاس والد (parent) می‌گویند، برای ارث‌بری کلاس والد هیچ تغییری نمی‌کند و برای ارث‌بری بعد از نام کلاس فرزند باید از کلمه کلیدی extends و سپس نام کلاس والد استفاده شود:

class BMW extends Car {
}

گاهی نیز به جای ارث‌بری می‌گویند کلاس متخصصان از کلاس کارمندان مشتق شده که به همان معنی ارث‌بری است. هر کلاس فرزند تمام متدها و پراپرتی‌های کلاس والد را به ارث می‌برد، علاوه بر این می‌تواند متدهای و پراپرتی‌های منحصر به خودش را داشته باشد و برخی از متدها را بازنویسی کند، مثلا کلاس متخصصان می‌تواند خروجی حقوق کارمندان را با مقدار پراپرتی‌های حق ماموریت، هزینه اسکان و رفت و آمد جمع کند و مقدار را به عنوان حقوق نهایی برگرداند. هر کلاس فقط می‌تواند از یک کلاس به صورت مستقیم ارث ببرد ولی از هر کلاس می‌توان بی‌نهایت بار ارث‌بری داشت، ضمنا از کلاس ارث‌بری شده نیز می‌توان دوباره ارث‌بری نمود، مثلا کلاس متخصصان را بار دیگر فرض کنید که چند دسته باشند، متخصصانی که ماموریت‌های داخل شهری می‌روند و حق اسکان ندارند، متخصصانی که ماموریت‌های داخل کشور می‌روند و متخصصانی که ماموریت خارج کشور می‌روند و هزینه‌های دیگری مانند عوارض خروج از کشور نیز اضافه می‌شود، هر کدام از این کلاس‌ها از یک کلاس می‌توانند ارث ببرند و آن کلاس متخصصان است.

دقت کنید که ارث‌بری فرآیند بی‌پایانی است. هر کلاس مشتق شده تمام متدها و پراپرتی‌های کلاس والدش را ارث می‌برد حتی متدها و پراپرتی‌هایی از کلاس جدش (کلاس والد والدش) که توسط والدش بازنویسی نشده باشد، طبیعتا در صورت بازنویسی، آن را از والد مستقیمش ارث می‌برد.

کپسوله‌سازی (Encapsulation)

مکانیزمی است که به وسیله آن متدها و پراپرتی‌ها به هم وصل می‌شوند و از تداخل خارجی و استفاده نامناسب جلوگیری می‌شود، در واقع استفاده از سطوح دسترسی با کلیدهای public، protected و private می‌باشد در ابتدای خطی که متد یا پراپرتی را تعریف می‌کنیم نوشته می‌شوند و نحوه دسترسی به آن پراپرتی یا متد را مشخص می‌کنند، منظور از دسترسی به پراپرتی، مقداردهی و خواندن مقدار آن است و منظور از دسترسی به متد فراخوانی آن است. در کلاس ممکن است هر سه نوع سطح دسترسی برای متغییرهای مختلف استفاده شوند.

class User {
    private $firstName;
    private $lastName;
    private $age;
    public function setFirstName($name) {
        $this->firstName = $name;
    }
    public function setLastName($family) {
        $this->lastName = $family;
    }
    public function setAge($age) {
        $this->age = $age;
    }
    public function showUserInfo() {
        echo 'My name is '. $this->firstName . ' ' . $this->lastName . ' and I'm ' $this->age;
    }
}
$mj = new User;
$mj->setFirstName('Mojtaba');
$mj->setLastName('Pakzad');
$mj->setAge(27);
$mj->showUserInfo();

کد مثال بالا دارای سه پراپرتی خصوصی است که نام و نام خانوادگی و سن فرد را نگهداری می‌کنند و با هر کدام برای مقداردهی یک متد دارند و یک متد نیز مسئول نمایش مفادیر پراپرتی‌ها مطابق شکل دلخواه ما است. اگر برخی از قسمت‌های مثال بالا را متوجه نمی‌شوید، نگران نباشید، در ادامه توضیح مفصل‌تری خواهم داد.

سطح دسترسی public

اگر متد یا پراپرتی دارای سطح دسترسی عمومی (public) باشد از داخل خود کلاس و کلاس‌های مشتق شده و هر جایی که کلاس include شده باشد، می‌توان به آن متد یا پراپرتی دسترسی داشت. منظور از دسترسی، در مورد پراپرتی مقداردهی و خواندن مقدار آن پراپرتی است و در مورد متد اجرای آن متد است.

سطح دسترسی protected

متدها و پراپرتی‌های دارای سطح دسترسی محفاظت شده (protected) فقط به خود کلاس و فرزندان آن اجازه دسترسی به متد یا پراپرتی محافظت شده را می‌دهد.

سطح دسترسی private

متدها و پراپرتی‌های دارای سطح دسترسی خصوصی (private) فقط داخل خود کلاس قابل دسترسی هستند و از هیچ جایی از برنامه غیر از متدهای خود کلاس نمی‌توان به سایر متدها و پراپرتی‌های خصوصی دسترسی داشت، حتی کلاس‌های مشتق شده نیز دسترسی به متدها و پراپرتی‌های خصوصی ندارند.

ثابت‌ها

کلاس‌ها علاوه بر متدها و پراپرتی‌ها می‌توانند دارای ثابت نیز باشند، نحوه تعریف آنها با استفاده از کلمه کلیدی const است و با استفاده از کلمه کلیدی self می‌توان به آن‌ها دسترسی داشت، ثابت‌ها قابل مقداردهی نیستند و این نوع دسترسی فقط خواندن مقدار آنها است، ثابت‌ها ذاتا استاتیک هستند و سطح دسترسی آن‌ها نیز public است:

class Student {
    const IQ = 110;

    function showIQ() {
        echo  self::IQ . "\n";
    }
}

echo Student::CONSTANT . "\n";

$class = new MyClass();
$class->showConstant();

echo $class::CONSTANT."\n";

در مثال بالا در متد با استفاده از کلمه کلیدی self و عملگر :: به ثابت دسترسی داشتیم و در بیرون از کلاس نیز با استفاده از نام کلاس و عملگر :: و نام ثابت، البته با ساخت شی نیز می‌شود به ثابت دسترسی داشت که خط 12 از طریق فراخوانی متد و خط 14 با کمک عملگر :: به آن دسترسی دارد. توصیه می‌شود نام ثابت‌ها را تماما با حروف بزرگ بنویسید.

عملگرها

عملگر تفکیک‌پذیری محدوده دسترسی ::

این عملگر برای دسترسی به اعضای استاتیک و ثابت‌های یک کلاس بدون ساخت شی از روی آن کاربرد دارد. این شیوه دسترسی دو نوع کاربرد دارد: در مثال ارث‌بری برای متخصصان، گفتیم که می‌توانیم حقوق را از فرمول استفاده شده در متد محاسبه حقوق کلاس والد استفاده کنیم و سپس با مقادیر مورد نظرمان جمع کنیم، یکی از این کاربردها دسترسی به مقادیر کلاس والد که بازنویسی نموده‌ایم است. در بیرون از کلاس می‌خواهیم به اعضای کلاس بدون ساخت شی دسترسی داشته باشیم.

کلمه کلیدی self

در مثال مربوط به ثابت‌ها، دیدیم که برای دسترسی از داخل کلاس به اعضا می‌توان از این کلمه کلیدی استفاده نمود.

کلمه کلیدی parent

برای دسترسی به عنصر والد نیز این کلمه کلیدی استفاده می‌شود، کافیست داخل متد مورد نظرتان از آن استفاده کنید.

کلمه کلیدی static

استاتیک (static) یا ایستا در برنامه‌نویسی شی‌گرا، اهمیت زیادی دارد. گاهی کلاس مجموعه‌ای از ابزارها است و فقط شامل متدها است و پراپرتی ندارد، پس نیازی به شی نداریم و می‌توانیم با استاتیک کردن متدها، آن‌ها را بدون ساخت شی استفاده کنیم، بعد از تعیین سطح دسترسی از این کلمه کلیدی استفاده می‌شود و برای دسترسی به متد، نام کلاس :: نام متد می‌نویسیم، ضمن اینکه در کلاس برای دسترسی به متدهای آن از self و عملگر :: و سپس نام متد استفاده می‌کنیم. یک مزیت متدهای ایستا نساختن شی است و قطعا مصرف حافظه ندارند. ولی در متدهای استاتیک نمی‌توانیم از $this استفاده کنیم. برای دسترسی به پراپرتی‌های ایستا باید از self::$field استفاده کنیم. اگر می‌خواهید یک پراپرتی را بین متدها به اشتراک بگذارید، آن را به صورت استاتیک تعریف کنید.

ثابت‌های جادویی

ثابت‌های جادویی با دو کاراکتر __ (Underscore) پشت سرهم شروع می‌شوند و با دو کاراکتر __ پشت سرهم پایان می‌یابند، دلیل نامگذاری آن‌ها به عنوان جادویی این است که با اینکه ثابت هستند ولی در هر قسمت از برنامه مقدار متفاوتی می‌گیرند: __LINE__ این ثابت در هر خطی از برنامه قرار بگیرد، شماره همان خط را برمی‌گرداند. __FILE__ مسیر کامل و نام فایل را برمی‌گرداند. __DIR__ دایرکتوری که فایل جاری در آن قرار دارد را برمی‌گرداند و معادل dirname(__FILE__) است. __FUNCTION__ نام تابع را برمی‌گرداند. __CLASS__ نام کلاس را به همراه فضای نام آن بر می‌گرداند. __TRAIT__ نام trait را به همراه فضای نام آن برمی‌گرداند. __METHOD__ در هر متدی از کلاس استفاده شود، نام آن متد از کلاس را برمی‌گرداند. __NAMESPACE__ فضای نام جاری را برمی‌گرداند.

متدهای جادویی

متدهای جادویی متدهایی هستند که کارهای خاصی را به صورت اتوماتیک انجام می‌دهند، این متدها با دو کاراکتر __ (Underscore) پشت سر هم شروع می‌شوند:

متد سازنده (Constructor)

متد سازنده با نام __construct()  و نوع public تعریف می‌شود. متد سازنده در هنگام ساخت شی، فراخوانی می‌شود و مقدار بازگشتی آن از نوع void است، یعنی هیچ مقداری را برنمی‌گرداند. هر زمانی که بخواهید عملیاتی در زمان ساختن شی از کلاس انجام شود، می‌توانید از متد سازنده استفاده کنید، البته برخی از مهمترین کاربردهای ﺳﺎﺯﻧﺪﻩ مقداردهی اولیه پراپرتی‌ها، تنظیم کوکی‌ها و ارتباط با پایگاه داده است. نکته: در کلاس‌های مشتق شده در صورت وجود متد سازنده، چون متد والد توسط فرزند بازنویسی می‌شود، در صورت نیاز به فراخوانی متد سازنده والد از کلمه کلید parent در کلاس فرزند استفاده کنید.

class Person {
    protected $family;
    public function __construct($family) {
        $this->family = $family;     
    }
}

class HisChild extends Person {
    private $name;
    public function __construct($name, $family) {
        parent::__construct($family);
        $this->name = $name;
    }
    public function showName() {
    	echo 'Your name is ' . $this->name . ' ' . $this->family;
    }
}

$mj = new HisChild('Mojtaba', 'Pakzad');
$mj->showName();

همانطوری که مشاهده نمودید متد سازنده می‌تواند مقادیری داشته باشد که در هنگام ساخت شی آن‌ها را به متد سازنده ارسال می‌کنیم.

متد مخرب (Destructor)

در نقطه مقابل متد سازنده، متد مخرب با نام __destruct() وجود دارد، البته استفاده از این متدها اجباری نیست ولی استفاده از آن‌ها به ما کمک زیادی می‌کند، فرض کنید در پایان اجرای یک اسکریپت می‌خواهید مقادیر پراپرتی‌ها در یک فایل یا در دیتابیس ذخیره شوند، استفاده از متد مخرب اینجا به ما کمک بسیاری خواهد نمود، در صورت از بین رفتن شی با استفاده از تابع unset و یا پایان اسکریپت (به دلیل اینکه PHP در پایان اجرای هر اسکریپت، متغییرها را از بین می‌برد تا خانه‌های اشتغال شده توسط اسکریپت آزاد شوند)، این متد فراخوانی می‌شود.

نکته: حتی اگر اسکریپت با تابع exit هم متوقف شود، باز هم متد مخرب فراخوانی می‌شود.
نکته: برای کاهش مصرف حافظه و در نتیجه پرفورمنس بهتر برنامه توصیه می‌کنیم به محض اتمام کار با یک شی آن را پاک کنید، در واقع اشیا نوعی از متغییرها هستند و مطابق طبیعت خود حافظه را اشغال می‌کنند.

سربارگیری (Overloading)

قبل از بررسی متدهای جادویی بعدی بهتر است ابتدا با مفهوم سربارگیری یا Overloading آشنا شوید، متدهای جادویی دیگری وجود دارند که با استفاده از آن‌ها می‌توان متدها و پراپرتی‌هایی داینامیک ساخت، بدین معنی که در بدنه کلاس نیستند ولی می‌توان آن‌ها را فراخوانی و استفاده کرد.

متدهای جادویی call و callStatic

متد __call() دو پارامتر می‌گیرد، پارامتر اول نام متد و پارامتر دوم آرگومان‌های ارسالی است، این متد زمانی فراخوانی می‌شود که متدی که فراخوانی کرده‌ایم وجود نداشته باشد، در واقع اگر متدی را فراخوانی کنیم که در بدنه کلاس وجود نداشته باشد، این متد جادویی فراخوانی می‌شود،متد __callStatic() نیز مانند متد __call() است با این تفاوت برای متدهای استاتیک کاربرد دارد.

class Users {
    public function __call($method, $args)
    {
        $args = implode(', ', $args);
        echo '<strong>Error:</strong> The method <strong>' . $method . '</strong> doesn\'t exist! ';
        echo 'You called it with args "' . $args . '" !<br>';
    }

    public static function __callStatic($method, $args)
    {
        $args = implode(', ', $args);
        echo '<strong>Error:</strong> The static method <strong>' . $method . '</strong> doesn\'t exist! ';
        echo 'You called it with args "' . $args . '" !<br>';
    }
}

$user = new Users;
$user->update(1, 'info@baversion');

Users::update(1, 'info@baversion');

در مثال بالا، کاربر متد update را برای تغییر ایمیل یوزر با شناسه 1 فراخوانی کرده است، ولی کلاس پاسخ داده که این متد وجود ندارد. دقت کنید که مقدار متغییر $method به حروف کوچک و بزرگ حساس است.

متدهای جادویی set و get

اگر بخواهیم با تعدادی داده کار کنیم، می‌توانیم آنها را در یک پراپرتی (از نوع private) تعریف نموده، سپس در متد سازنده برای هر داده یک اندیس در این پراپرتی اختصاص دهیم، برای مقداردهی از متد جادویی __set() استفاده می‌کنیم که دو پارامتر می‌گیرد، پارامتر اول نام و پارامتر دوم مقدار است، حالا هر داده را مورد پردازش قرار می‌دهیم و در نهایت نیز با استفاده از متد جادویی __get() مقدار هر داده را با نام مورد نظر می‌توانیم از اندیس‌های آن پراپرتی برگردانیم. مقداردهی در خارج از برنامه بدین صورت است که نام اندیس مورد نظر را از طریق شی فراخوانی کرده و برابر مقدار مورد نظرمان قرار می‌دهیم:

$obj1->name = ‘value’;

دقت کنید که می‌توان مقدار اندیس‌ها را نیز برای عدم حساسیت به کوچک و بزرگ تبدیل به حروف کوچک نمود و با حروف کوچک کار کرد، در اینصورت اگر حروف نام اندیس را کوچک و بزرگ بنویسیم، مشکلی وجود نخواهد داشت.

class Car {
    private $fields;
    public function __construct() {
        $this->fields = array(
            'factory' => NULL,
            'model' => NULL,
            'color' => NULL,
        );
    }
    public function __set($field, $value) {
        $this->fields[$field] = $value;
    }
    public function __get($field) {
        return $this->fields[$field];
    }
}

$car = new Car;
$car->factory = 'BMW';
$car->model = 'X2';
$car->color = 'Misano Blue Metallic';

echo 'I bought a ' . $car->factory . ' ' .  $car->model . '<br>' . PHP_EOL;
echo 'The color of my car is ' . $car->color;

در کد بالا ما یک شی به نام car ساخته‌ایم، سپس با کمک آن، به پراپرتی‌های کلاس مقداردهی کرده‌ایم، در نهایت آن‌ها را استفاده کرده‌ایم. دقت کنید که پراپرتی‌ها در کلاس وجود ندارند و توسط متدهای جادویی و آن‌ها را استفاده کرده‌ایم، در واقع این متدها کمک کرده‌اند تا پراپرتی‌هایی که وجود ندارند را بسازیم.

متدهای جادویی isset و unset

متد جادویی __isset() زمانی فراخوانی می‌شود که برای پراپرتی‌هایی که دسترسی به آن‌ها نداریم از توابع isset() یا empty() استفاده کنیم و متد جادویی __unset() نیز زمانی که برای پراپرتی‌هایی که به آن‌ها دسترسی نداریم از تابع __unset() استفاده کنیم. عدم دسترسی هم می‌تواند ناشی از عدم وجود آن پراپرتی و هم سطح دسترسی آن پراپرتی باشد.

class Student {
    public $id;
    private $avg;

    public function __construct() {
        $this->id = 0;
        $this->avg = 0;
    }

    public function __isset($field)
    {
        if(isset($this->$field)) {
            echo '<strong>Error (in __isset):</strong> Direct access to <strong>' . $field . '</strong> not permitted!<br>';
        } else {
            echo '<strong>Error (in __isset):</strong> The field <strong>' . $field . '</strong> doesn\'t exist!<br>';
        }
    }

    public function __unset($field)
    {
        if(isset($this->$field)) {
            echo '<strong>Error (in __unset):</strong> Direct access to <strong>' . $field . '</strong> not permitted!<br>';
        } else {
            echo '<strong>Error (in __unset):</strong> The field <strong>' . $field . '</strong> doesn\'t exist!<br>';
        }
    }

    public function showFields() {
        echo 'The fields are:<br>';
        echo '<strong>id :</strong> ' . $this->id . '<br>'; 
        echo '<strong>avg :</strong> ' . $this->avg . '<br>';
    }
}

$stu = new Student;

if(isset($stu->id)) {
    $stu->id = 1;
}

echo '<hr>';

if(isset($stu->firstName)) {
    $stu->firstName = 'Mojtaba';
}

echo '<hr>';

if(isset($stu->avg)) {
    $stu->avg = 20;
}

echo '<hr>';

$stu->showFields();

echo '<hr>';

unset($stu->avg);

echo '<hr>';

$stu->showFields();

مثال بالا به راحتی مفاهیم گفته شده رو به شما نشان می‌دهد، قبل از خواندن ادامه مطلب حتما این مثال را اجرا کنید. توضیحات: در خط 1 کلاس را تعریف کرده‌ایم. در خط 2 و 3 دو پراپرتی با دو سطح دسترسی متفاوت ذکر شده‌اند. در خط 5 متد سازنده به پراپرتی‌ها مقداردهی اولیه می‌کند. در خط 10 متد جادویی __isset() نوشته شده و داخل آن با استفاده از تابع isset() (با متد جادویی __isset() اشتباه گرفته نشود)، بررسی می‌کند که دلیل عدم دسترسی به پراپرتی، عدم وجود آن است یا سطح دسترسی و پیام متناسب با آن را نشان می‌دهد. دقت کنید که $this->field() به پراپرتی‌ای در کلاس با نام filed اشاره دارد، اگر بخواهیم به مقدار پارامتر field در بدنه این متد دسترسی داشته باشیم باید قبل از آن حتما یک علامت قرار $ دهیم، همانطور که در خط 12 می‌بینید این کار باعث اشاره به پراپرتی‌های کلاس با نامی برابر مقدار این پارامتر می‌شود. (اگر این قسمت را متوجه نشدید، نگران نباشید در ادامه بیشتر توضیح خواهیم داد.) در خط 19 متد جادویی __unset() نوشته شده که بدنه آن مانند بدنه متد جادویی __isset() با کمی تغییرات در متن پیام‌ها نوشته شده است و نیازی به توضیح ندارد. در خط 35 یک شی از روی این کلاس ساخته‌ایم. در خط 37 از یک دستور شرطی استفاده شده است، این دستور از تابع isset() برای بررسی وجود پراپرتی id استفاده کرده، اینجا ابتدا مقدار true برمی‌گرداند و بدنه شرط اجرا می‌شود، ولی متد جادویی __isset() فراخوانی نمی‌شود. در خطوط 41 و 47 و 53 و 57 و 61 برای درک بهتر برنامه قسمت‌های مختلفی که پیام نشان می‌دهند را از هم جدا کرده‌ام. در خط 43 دستوری مشابه خط 37 وجود دارد، با این تفاوت که شرط به علت عدم وجود پراپرتی مقدار false را برمی‌گرداند، حالا نوبت فراخوانی خودکار متد __isset() است که در بدنه اش نوشته‌ایم که چون این پراپرتی وجود ندارد، پیام قسمت else را نشان دهد. دقت کنید که اگر در خط 12 به جای this->$field() از this->field() استفاده می‌کردیم، همیشه در شرط داخل بدنه این متد جادویی قسمت else اجرا می‌گردید چون پراپرتی‌ای با نام field وجود ندارد و دستور دوم به آن پراپرتی اشاره دارد ولی دستوری که استفاده می‌کنیم، مقدار این پارامتر که در این فراخوانی firstName است را استفاده می‌کند. خط 49 دستور مشابه خط 43 رفتار می‌کند ولی با این تفاوت که پیام قسمت اول نمایش داده می شود، یعنی خطای عدم دسترسی به علت سطح دسترسی. خط 55 و 63 متد showFields را قبل و بعد از استفاده از تابع unset() فراخوانی می‌کنند، این متد را تعریف کرده‌ایم تا مقادیر پراپرتی‌ها را نمایش دهد. دقت کنید که ما مقادیر را ابتدا با متد سازنده مقداردهی کرده بودیم و در خط 37 دیدیم که بعد از اجرای شرط به پراپرتی id مقدار جدیدی داده شد. خط 59 تابع unset() را فراخوانی می‌کند و اثر آن به علت عدم دسترسی به پراپرتی مربوطه، فراخوانی متد جادویی __unset() است که پیام مورد نظر را نمایش می‌دهد ولی  اگر به جای پراپرتی avg، پراپرتی id را در این خط می‌نوشتیم، به علت سطح دسترسی عمومی آن، این پراپرتی از بین می‌رفت و متد جادویی __unset() نیز اجرا نمی‌گردید.

توابع serialize و unserialize

تابع serialize برای تبدیل یک آرایه یا شی، به یک رشته استفاده می‌شود و تابع unserialize برای تبدیل رشته serialize شده به حالت اولیه آن استفاده می‌شود. یکی از کاربردهای رشته serialize شده ذخیره آن در دیتابیس است، این اطلاعات مهم هستند ولی ساخت پراپرتی برای آن‌ها ضروری نیست، و نیازی به جستجو پراپرتی‌ها بر اساس آن‌ها ندارید.

متدهای جادویی sleep و wakeup

حتما دقت کرده‌اید که درک راحت‌تر متدهایی که کار برعکس یا مشابه هم را انجام می‌دهند، با هم توضیح می‌دهم، در اینجا نیز این دو متد جادویی برعکس هم کار می‌کنند. وقتی یک شی را serialize کنیم، ابتدا دنبال متد جادویی __sleep() می‌گردد و با استفاده از آن می‌توان پراپرتی‌های مورد نظرمان برای serialize شدن را انتخاب کنیم و به سایر گزینه‌ها اجازه serialize ندهیم. این متد جادویی نمی‌تواند پراپرتی‌های خصوصی کلاس والد خود را برگرداند.

class Citizations {
    public $idNumber;
    public $firstName;
    public $lastName;
    public $homeTown;

    public function __construct($idNumber, $firstName, $lastName, $homeTown) {
        $this->idNumber = $idNumber;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->homeTown = $homeTown;
    }

    public function logData($data) {
        $fileName = $this->idNumber . '.txt';
        file_put_contents($fileName, $data);
        echo 'The data saved in a file.<br>';
    }

    public function __sleep() {
        echo 'Calling method <strong>__sleep.</strong><br>';
        return array('firstName', 'lastName', 'homeTown');
    }
}

$person = new Citizations(1, 'Mojtaba', 'Pakzad', 'Tehran');

$data = serialize($person);

$person->logData($data);

در مثال بالا داده‌های مربوط به هر کاربر را در یک فایل با نام شناسه id فرد ذخیره کردم و برای جلوگیری از تکرار داده‌ها، شناسه را سریالایز نکردم. نحوه ذخیره سشن‌ها در PHP نیز تقریبا مشابه روش بالا است.

class DBConnection
{
    protected $link;
    private $dsn, $server, $dbName, $dbUsere, $dbPass;
    
    public function __construct($server, $db, $user, $pass)
    {
        $this->dsn = 'mysql:host=' . $server .';db=' . $db;
        $this->server = $server;
        $this->dbName = $db;
        $this->dbUser = $user;
        $this->dbPass = $pass;
        $this->connect();
    }
    
    private function connect()
    {
        try {
            $this->link = new PDO($this->dsn, $this->dbUser, $this->dbPass);
            $this->link->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            echo "Connected successfully.<hr>"; 
        }
        catch(PDOException $e)
        {
            echo "Connection failed: " . $e->getMessage() . '<hr>';
        }
    }
    
    public function __sleep()
    {
        echo 'Calling __sleep method.<hr>';
        return array('dsn', 'dbUser', 'dbPass');
    }
    
    public function __wakeup()
    {
        echo 'Calling __wakeup method.<br>';
        $this->connect();
    }
}
$DB = new DBConnection('localhost', 'wp', 'root', '');
$a = (serialize($DB));
unserialize($a);

یکی از کاربردهای متد جادویی __wakeup() بازسازی هرگونه اتصال به دیتابیس است که ممکن است در حین سریالایز شدن از بین رفته باشد و اجرای دستورات و مقداردهی‌های اولیه نیز کاربرد دیگری از آن است.

متد جادویی toString و invoke

هرگاه با یک شی مانند یک رشته رفتار شود، متد جادویی __toString() فراخوانی می‌شود و به کلاس اجازه می‌دهد تا نحوه رفتار متقابل را تعیین کند، مثلا یک خطای سفارشی بسازیم یا مقداری را برگردانیم.

class collaborators {
    private $firstName, $lastName, $email, $roll;

    public function __construct($firstName, $lastName, $email, $roll) {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->email = $email;
        $this->roll = $roll;
    }

    public function showData() {
        echo 'Your collaborator name is ' . $this->firstName . ' ' . $this->lastName . '<br>';
        echo 'His email is <em>' . $this->email . '</em> and his roll is <em>' . $this->roll . '</em>';
    }

    public function __toString() {
        $msg = 'There is a method to see data.<br>';
        $msg = 'Call method <strong>showData</strong> : $obj->showData()';
        return $msg;
    }

}

$coWorker = new Collaborators('mojtaba', 'pakzad' , 'info@baversion.com', 'Back-end developer');

echo $coWorker;

echo '<hr>';

$coWorker->showData();

مثال بالا در هنگام استفاده اشتباه برای خروجی گرفتن، پیامی را همراه نحوه صحیح خروجی گرفتن نمایش می‌دهد. حالا اگر بخواهیم از شی، به عنوان تابع استفاده کنیم، متد جادویی __invoke() فراخوانی می‌شود:

class Math {
    private $number, $factorial;

    public function __construct() {
        $this->number = 0;
        $this->factorial = 1;
    }

    public function factorial($number) {
        for ($i = $number; $i > 1; $i--) { 
            $this->factorial *= $i;
        }
        return $this->factorial;
    }

    public function __invoke($number) {
        echo 'To claculate Factorial of ' . $number . ', you must call <strong>factorial</strong> method! : <code>$obj->factorial($number)</code><br>';
        echo 'The factorial of ' . $number .' is: ' . $this->factorial($number);
    }
}
$obj = new Math();
$obj(7);

می‌دانیم که فاکتوریل برای اولین بار توسط کریستین کرامپ معرفی گردید. در مثال بالا فرض کرده‌ام کاربر اشتباها شی را به عنوان تابعی برای محاسبه فاکتوریل فراخوانی کرده، ما پیام مناسبی به کاربر نمایش داده‌ایم که هم روش صحیح فراخوانی متد فاکتوریل، و مقدار فاکتوریل عدد مورد نظر  کاربر را به وی نشان می‌دهد. می‌توانستیم یک پیام با این مضمون نشان دهیم که در صورتی که نحوه استفاده اشتباه است، سپس راهنما و متدهای قابل دسترسی کاربر را نمایش دهیم.

متد جادویی set_state

یادآوری: تابع var_export، متغییرها را به شکلی مناسب برای استفاده در PHP (به عنوان کد) نمایش می‌دهد و خروجی آن بسته به پارامتر دوم آن قابل بازگشت و نمایش است.

class Persons {
    private $persons, $tabNum;

    public function __construct() {
        $this->persons = array();
        $this->tabNum = 1;
    }

    public function addPerson($name, $email) {
        $this->persons[] = array(
            'name' => $name,
            'email' => $email 
        );
    }

    private function createIndent() {
        $indentDepth = 4 * $this->tabNum;
        $indent = '';
        for ($i = 0; $i < $indentDepth; $i++) { 
            $indent .= '&nbsp';
        }
        return $indent;
    }

    private function getStructure($args) {
        $this->tabNum++;
        $msg = 'array (<br>';
        foreach($args as $key => $value) {
            $msg .= $this->createIndent();
            $msg .= '[' . (is_int($key) ? $key : '\'' . $key . '\'') . ']';
            $msg .= ' => ';
            $msg .= (is_array($value) ? $this->getStructure($value) : $value) . ',<br>' . PHP_EOL;
        }
        $msg .= $this->createIndent();
        $msg .= ')';
        $this->tabNum--;
        return $msg;
    }

    public function __set_state($args) {
        $obj = new Persons;
        return $obj->getStructure($args['persons']);
    }
}

$obj = new Persons;
$obj->addPerson('Mojtaba', 'info@baversion.com');
$obj->addPerson('Mojtaba2', 'info2@baversion.com');
$obj->addPerson('Mojtaba3', 'info3@baversion.com');

eval('$data = ' . var_export($obj, true) . ';');
echo $data;

در مثال بالا مقادیر داخل پراپرتی persons را به شکل مناسب نمایش داده‌ام، البته این‌کار را با پیمایشگرها بسیار ساده‌تر می‌توان انجام داد، ولی اینجا بحث نمایش دادن یکی از کاربردهای این متد جادویی __set_state() بوده نه پیمایش پراپرتی‌های کلاس با کمک Iteratorها.

متد جادویی clone

یادآوری: می‌دانیم که شی در واقع نوعی متغییر است، در حالت عادی اگر مقدار یک شی را در یک متغییر بریزیم، در واقع یک شی داریم که دو لیبل دارد (هر دو به یک خانه از حافظه اشاره دارند)، یعنی با تغییر هر کدام مقدار دیگری نیز تغییر می‌کند، اگر بخواهیم که با تغییر یک شی مقدار دیگری تغییر نکند، باید از کلمه کلیدی clone استفاده کنیم.

متد جادویی __clone() نیز به کلاس اجازه می‌دهد تا رفتار متناسبی را در هنگام استفاده از کلمه کلیدی clone از خود نشان دهد:

class Student {
    private $email;

    public function __construct() {
        echo 'Construct method ran.<br>';
    }

    public function setEmail($email) {
        $this->email = $email;
    }

    public function getEmail() {
        return $this->email; 
    }
    
    public function __clone() {
        echo 'Cloning happend!';
    }

    public function __destruct() {
        echo 'Destruct method ran.<br>';
    }
}

echo '<strong>Create Objects:</strong><br>';
$mj = new Student;
$mojtaba = $mj;
$milad = clone $mj;

echo '<hr>';
echo '<strong>Set emails:</strong><br>';

$mj->setEmail('mojtaba@baversion.com');
$mojtaba->setEmail('info@baversion.com');
$milad->setEmail('milad@baversion.com');

echo '<hr>';
echo '<strong>Getting emails:</strong><br>';

echo '<strong>mj:</strong> ' . $mj->getEmail() . '<br>';
echo '<strong>mojtaba:</strong> ' . $mojtaba->getEmail() . '<br>';
echo '<strong>milad:</strong> ' . $milad->getEmail() . '<br>';

echo '<hr>';
echo '<strong>End of script:</strong><br>';

مثال بالا، یک پیام در هنگام کلون کردن نشان می‌دهد.

متد جادویی debugInfo

متد جادویی __debugInfo() آخرین متد جادویی است و اگر یک شی را توسط تابع var_dump فراخوانی کنیم، این متد اجرا می‌شود:

class CoWorker {
    private $name, $graduated;

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

    public function __debugInfo() {
        $fields = array(
                'name' => $this->name,
                'graduated' => $this->graduated
        );
        echo '<strong>You ran var_dump() function:</strong><br>';
        echo '<strong>Results:</strong> name is <em>' . $fields['name'] . '</em> and graduated is <em>' . $fields['graduated'] . '</em><hr>';
        return $fields;;
    }
}

$coWorker = new CoWorker('Mojtaba Pakzad');
var_dump($coWorker);

اگر این مطلب را از ابتدا دنبال کرده باشید باید کد بالا را به راحتی درک کنید، در غیر این‌صورت توصیه می‌کنیم مطلب را مجددا بخوانید.

جمع‌بندی

با رعایت اصول شی‌گرایی کیفیت، سرعت و امنیت کدهای خود را بالاتر ببرید، متدهای جادویی خصوصا متد سازنده بسیار کاربردی هستند، حتما استفاده از آن‌ها را تمرین کنید. در راهنمای مستندسازی با phpDoc گفتم که برخی تگ‌های PhpDoc هستند که مخصوص کلاس هستند، توصیه می‌کنم راهنمای مستندسازی با phpDoc را بخوانید تا بتوانید کدهای خود را به خوبی کامنت‌گذاری کنید، همچنین بعد از این مطلب می‌توانید مباحث پیشرفته شی‌گرایی در PHP یا معرفی الگوی معماری MVC را بخوانید.

تگ‌ها