مباحث پیشرفته شی‌گرایی در PHP

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

مباحث پیشرفته شی‌گرایی

چندریختی (polymorphism)

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

نکته: متغییرها ماهیت متغییر دارند و دلیل تعریف آن‌ها، تغییر مقدارشان در صورت نیاز است، پس امکان تعریف کلمه کلیدی final برای آن‌ها وجود ندارد و همچنین ثابت‌ها نیز قابل تغییر نیستند و تعریف کلمه کلیدی final برای آن‌ها بی‌معنی است. تنها برای متدها و کلاس‌ها می‌توان از کلمه کلیدی final استفاده نمود.
final class Animal {
    final public function showName() {
    }
}

نیازی نیست متدهای کلاسی که final است را final استفاده کنید.

نکته: اگر متدی با سطح دسترسی private در کلاس والد داشته باشید، در حالت عادی به متد والد دسترسی نخواهید داشت، ولی می‌توانید در کلاس فرزند آن را بازنویسی کنید. اما اگر آن متد در کلاس والد را final تعریف کنید، در کلاس مشتق شده، حتی قابل بازنویسی نیست.

کلاس انتزاعی (abstract)

انتزاع

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

کلاس انتزاعی

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

abstract class Car {
    abstract protected function setHeight($height);
}
class BMW extends Car
{
    private $height;
    protected function setHeight($height) {
        $this->height;
    }
}
$car = new BMW();

در متدهای abstract به جای {} در انتهای متدها ; وجود دارد. کار این کلاس بررسی وجود متدهای مورد نظر ما (با سطح دسترسی) در کلاس‌های مشتق شده است.

نکته: کلاس و متد abstract را نمی‌توان final و private نمود، چون باید از آن ارث برده شود.
نکته: کلاس مشتق شده باید دارای سطح دسترسی معادل یا ضعیفتری نسبت به والدش باشد، یعنی اگر متدی در کلاس والد protected باشد، کلاس همان متد در کلاس مشتق شده (فرزند)، می‌تواند دارای سطح دسترسی protected یا public باشد ولی با سطح دسترسی private نمی‌تواند تعریف شود.
نکته: شاید به نظرتان عجیب بیاید که وقتی متدی abstract تعریف می‌شود، کلاس حاوی آن متد نیز باید abstract باشد، ولی به خاطر داشته باشید که از کلاس abstract نمی‌توان شی ساخت و حتما برای دسترسی به متدهای آن، باید توسط کلاس دیگر ارث برده شود.
abstract class Product {
    protected $name;
    protected $price;
 
    public function __construct($name, $price) {
        $this->name = $name;
        $this->price  = $price;
    }
 
    abstract public function getDetail();

    final protected function createTable($args) {
            echo '<table style="border-collapse: collapse;width: 20%;text-align:center">' . PHP_EOL;
            echo '<thead>' . PHP_EOL;
            echo '<tr>' . PHP_EOL;
            echo '<th>Name</th>' . PHP_EOL;
            echo '<th>Value</th>' . PHP_EOL;
            echo '<tr>' . PHP_EOL;
            echo '</thead>' . PHP_EOL;
        foreach ($args as $key => $value) {
            echo '<tr>' . PHP_EOL;
            echo '<td>' . $key . '</td>' . PHP_EOL;
            echo '<td>' . $value . '</td>' . PHP_EOL;
            echo '<r>' . PHP_EOL;
        }
            echo '</table>' . PHP_EOL;
    }
}

class Laptop extends Product {
    private $cpu;
    private $ram;
    private $monitor;
 
    public function __construct($name, $price, $cpu, $ram, $monitor) {
        parent::__construct($name, $price);
        $this->cpu     = $cpu;
        $this->ram     = $ram;
        $this->monitor = $monitor;
    }
 
    public function getDetail() {
        echo 'The laptop Details :';
        $args = array(
            'name'    => $this->name,
            'price'   => $this->price,
            'cpu'     => $this->cpu,
            'ram'     => $this->ram,
            'monitor' => $this->monitor,
        );
        parent::createTable($args);
    }
}
class Charger extends Product {
    private $outputVoltage;
    private $inputVoltage;
    private $current;
 
    public function __construct($name, $price, $outputVoltage, $inputVoltage, $current){
        parent::__construct($name, $price);
        $this->outputVoltage = $outputVoltage;
        $this->inputVoltage  = $inputVoltage;
        $this->current       = $current;
    }
 
    public function getDetail() {
        echo 'The charger Details :';
        $args = array(
            'name'              => $this->name,
            'price'             => $this->price,
            'outputVoltage'     => $this->outputVoltage,
            'inputVoltage'      => $this->inputVoltage,
            'current'           => $this->current,
        );
        parent::createTable($args);
    }
}

$product1 = new Laptop('Macbook', '2500', 'corei7', '16', '15.6');
echo $product1->getDetail();
echo '<hr>';
$product2 = new Charger('Apple', '15', 5, 220, 1);
echo $product2->getDetail();

در مثال بالا ما یک کلاس برای محصولات داریم، این کلاس abstract است و از آن نمی‌توان شی ساخت، کلاس‌های Laptop و Charger که دو نوع از محصول ما هستند از کلاس Product مشتق شده‌اند، هر محصول ویژگی‌های خاص خود را دارد. از کاربردهای دیگر کلاس abstract، استفاده از آن برای کلاس پیام‌ها است و بستگی به سرویس ارسال پیام، کلاس‌هایی برای ایمیل و sms می‌نویسیم،کلیت یکی است و نحوه ارسال توسط کلاس‌های مشتق شده تعیین می‌شود.

رابط یا اینترفیس (interface)

رابط یا اینترفیس با کلمه کلیدی interface تعریف می‌شود و در بدنه آن فقط می‌توان متدهایی را نوشت که باید پیاده سازی شوند:

interface DB {
    public function Create();
    public function Read();
    public function Update();
    public function Delete();
}

تفاوت abstract با interface در این است که در بدنه رابط به هیچ عنوان نمی‌توان متدی را پیاده سازی کرد، ولی در کلاس‌های انتزاعی می‌توان متدی را که abstract نباشد در بدنه آن‌ها پیاده‌سازی نمود. تفاوت دیگر این است که هر کلاس فقط و فقط می‌تواند از یک کلاس انتزاعی مشتق شود، ولی هر کلاس می‌تواند متدهای بیش از یک رابط را پیاده‌سازی کند. در مثال زیر رابط بالا را پیاده‌سازی کرده‌ایم:

class Post implements DB {
    public function Create() {
        // ...
    }
    public function Read() {
        // ...
    }
    public function Update() {
        // ...
    }
    public function Delete() {
        // ...
    }
}

در صورت نیاز کلاس شما می‌تواند هم از یک کلاس دیگر ارث ببرد و هم تعدادی رابط را پیاده‌سازی کند (بین نام رابط‌ها کاما , قرار دهید). دقت کنید که برای رابط چیزی به نام مشتق شدن و ارث بردن وجود ندارد و باید پیاده‌سازی شوند.

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

برای نامگذاری رابط‌ها نیز قراردادهای مشخص وجود دارد که بسته به پروژه متغییر است، مثلا آن‌ها را در پوشه‌ای به نام int قرار می‌دهند. روش دیگر افزودن حرف i در ابتدای نام رابط است که این حرف i در ابتدای نام فایل وجود ندارد.

مقایسه اشیا

چهار عملگر == و != و === و !== برای مقایسه اشیا با هم وجود دارد:

  • اگر دو شی از یک کلاس داشته باشیم، فقط نتیجه مقایسه آن دو، با استفاده از عملگرهای == و !== برابر true خواهد بود.
  • اگر یک شی از یک کلاس را با کپی آن شی مقایسه کنیم، فقط نتیجه مقایسه آن دو با عملگرهای == و === برابر true خواهد بود.
  • اگر دو شی از دو کلاس متفاوت داشته باشیم، فقط نتیجه مقایسه آن دو با عملگرهای != و !== برابر true خواهد بود.
نکته: با استفاده از کلمه کلیدی is_a می‌توان بررسی کرد که آیا یک شی یا کلاس، از نوع یک شی یا کلاس است.
نکته: برای بررسی اینکه یک شی با استفاده از کلاس خاصی ساخته شده است، نام شی، کلمه کلیدی instanceof و نام کلاس را می‌نویسیم، که true یا false برمی‌گرداند.
نکته: هنگام استفاده از متدهای داخل یک کلاس مشتق شده از کلاس انتزاعی، می‌توانیم با استفاده از instanceof بررسی کنیم که آیا شی ساخته شده از کلاس مشتق شده، از نوع (نمونه‌ای از) کلاس انتزاعی است، سپس از این متدها استفاده کنیم، چون کلاس انتزاعی، کلاس مشتق شده را مجبور به استفاده از این متدها می‌کند.

تفاوت instanceof با is_a اینست که instanceof کلمه کلیدی است و سرعت بیشتری دارد، is_a یک پارامتر سوم با مقدار پیش فرض false است، اگر این پارامتر را true قرار دهیم، می‌توانیم بررسی کنیم که آیا یک کلاس از نوع کلاس دیگر است یا نه.

کلاس‌های ناشناس یا بی‌نام (Anonymous)

این قابلیت در PHP 7 اضافه شده است و وقتی مفید و کاربردی هستند که شی ساده و یکنواختی نیاز داریم بسازیم. می‌توان گفت دلیل استفاده از کلاس‌های ناشناس تطبیق‌پذیری است.

class Logger
{
    public function log($msg)
    {
        echo $msg;
    }
}

$util->setLogger(new Logger());

// PHP 7+ code
$util->setLogger(new class {
    public function log($msg)
    {
        echo $msg;
    }
});

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

زنجیره متدها (method chaining)

قابلیت زنجیر کردن متدها به صورت پشت سر هم که به آن Method Chaining می‌گویند در اکثر زبان‌های برنامه‌نویسی وجود دارد:

class Car {
    private $name;
    private $model;
    private $price;

    public function setName($name) {
        $this->name = $name;
        return $this;
    }
    
    public function setModel($model) {
        $this->model = $model;
        return $this;
    }
    
    public function setPrice($price) {
        $this->price = $price;
        return $this;
    }

    public function __toString() {
        return 'I bought a ' . $this->name . ' ' . $this->model . '. The new model will be priced at $' . $this->price . '.';
    }
}

$car = new Car;
echo $car->setName('Lamborghini')->setPrice('45,000')->setModel('Aventador');

در مثال بالا ترتیب زنجیر کردن متدها مهم نیست و امتیاز این روش قابلیت جابجانویسی متدها است، تکنیکی که این قابلیت را به ما می‌دهد استفاده از return $this برای هر متد است. دقت کنید که ما شی را مستقیما مورد استفاده قرار داده‌ایم و همانطور که می‌دانید اینکار باعث فراخوانی متد جادویی invoke می‌شود. یکی از کاربردهای خیلی مفید این قابلیت، استفاده از آن در کلاس دیتابیس است که پیاده‌سازی آن بسیار ساده است:

Class Database {
    private $select = array();
    private $table, $whereClause, $limit, $order;

    public function select() {
        $this->select = func_get_args();
        return $this;
    }

    public function from($table) {
        $this->table = $table;
        Return $this;
    }

    public function where($where = null) {
        $this->whereClause = $where;
        return $this;
    }

    Public function limit($limit) {
        $this->limit = $limit;
        return $this;
    }

    public function order($order) {
        $this->order = $order;
        return $this;
    }

    public function result() {

        $query = 'SELECT ' . (empty($this->select) ? '*' : implode(',', $this->select));
        $query .= ' FROM ' . $this->table;

        if(!empty($this->whereClause)) {
            $query .= ' WHERE (' . $this->whereClause . ')';
        }

        if(!empty($this->order)) {
            $query .= ' ORDER BY ' . $this->order;
        }

        if(!empty($this->limit)) {
            $query .= ' LIMIT ' . $this->limit;
        }

        echo 'The generated query is:<br>' . PHP_EOL;
        echo $query . '<br>' . PHP_EOL;
    }
}

$db = new Database();
$db->select('title', 'slug', 'content')->from('posts')->where('catid = 23')->order('id DESC')->result();
echo '<hr>';
$db->select()->from('posts')->where()->order('id DESC')->limit(7)->result();

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

بارگذاری خودکار کلاس

در اصول شی‌گرایی در PHP گفتیم که در پروژه‌های مختلف فایل‌ها به صورت مختلف و بر اساس نظم و قاعده مشخصی نامگذاری و پوشه‌بندی می‌شوند، یک مزیت این‌کار امکان بارگذاری خودکار فایل‌های مربوط به کلاس‌ها است. بارگذاری فایل‌ها با کمک دستورات include و require کاری خسته کننده است که گاهی خطوط، فضای زیادی از فایل را اشغال می‌کند و شاید گاهی فایلی را فراموش کنید که include کنید یا به آن نیازی نداشته باشید و بدون دلیل include شود. دستور spl_autoload_register یک تابع از توابع کتابخانه استاندارد پی‌اچ‌پی (Standard Php Library) است، این دستور یک پارامتر می‌گیرد که نام یک تابع است و هر وقت کلاسی تعریف کنیم، مفسر پی اچ پی ابتدا در کلاس‌های داخلی خودش به دنبال آن می‌گردد و اگر آن را پیدا نکند، تابع ثبت شده را با پارامتر نام کلاس فراخوانی می‌کند.

function autoload( $class_name ) {
    $file_name = $class_name . '.php';
    if( file_exists( $file_name ) ) {
        require_once $file_name;
    }
}
spl_autoload_register( 'autoload' );

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

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

در اصول شی‌گرایی در PHP گفتیم که سربارگیری، قابلیتی است که به ما امکان ساخت متدها و پراپرتی‌های داینامیکی (پویا) را می‌دهد که این کار توسط متدهای جادویی انجام می‌شود، فرض کنید پراپرتی‌ای دارید که می‌خواهید قبل از مقداردهی آن را فیلتر کنید، یک راه برای انجام اینکار private قرار دادن پراپرتی و استفاده از متد جادویی call است:

class Overloading {
	private $name;

	public function __set($name, $field) {
		$this->name = $field;
	}

	public function getName() {
		echo 'My name is ' . $this->name;
	}
}

$ol = new Overloading;
$ol->name = 'Mojtaba';
$ol->getName();

انقیاد استاتیک دیررس (late static bindings)

انقیاد جمع قید و به معنی مقید و مطیع ساختن است. به انقیاد پیش از اجرای برنامه انقیاد زودرس یا ایستا و به انقیاد صورت گرفته در زمان اجرای برنامه، انقیاد دیررس یا پویا می‌گویند. انقیاد استاتیک دیررس یکی از ویژگی‌های PHP است که از ورژن 5.3.0 به آن اضافه شده است که برای مقید کردن یک شی یا پراپرتی به یک مقدار خاص استفاده می‌شود. این ویژگی به ما اجازه می‌دهد تا متدهای استاتیک را از کلاس والد و با ارجاع کلاس فرزندی که فراخوانی شده، به ارث برسانیم، یعنی می‌توانیم یک کلاس انتزاعی (abstract) داشته باشیم که دارای متدهای استاتیک است و با کمک static::method()  به جای استفاده از self::method() به کلاس فرزندش ارجاع بدهید. البته نیازی نیست که کلاس والد حتما abstract باشد و این فقط یک نمونه بود، برای درک بهتر بزارید یک مثال بزنم:

class Country {
	protected static $capital = 'an unknown city';

	public static function getCapital(){
		return self::$capital;
	}
}
 
class Iran extends Country {
	protected static $capital = 'Tehran';	
}
 
echo 'The capital of Iran is ' . Iran::getCapital();

کد بالا، نتیجه غیرمنتظره‌ای را به شما نشان خواهد داد:

The capital of Iran is an unknown city

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

تعیین نوع پارامترهای ورودی (Type declarations)

این ویژگی از PHP 5 با نام Type Hinting شناخته می‌شود و تعریف نوع پارامترهای ورودی متدها و توابع، قابلیتی است که به ما می‌دهد. نوع پارامتر را می‌توانید نام یک کلاس یا رابط، رشته، عدد صحیح و ... قرار دهید.

نکته: استفاده از آن اجباری نیست ولی اگر استفاده کنید و نوع ورودی را اشتباه وارد کنید، خطا نمایش داده می‌شود.
class C {}
class D extends C {}
class E {}
function f(C $c) {
    echo get_class($c) . '<br>';
}

f(new C);
f(new D);
f(new E);

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

ارث‌بری از چند والد با کمک ویژگی‌های خاص (Trait)

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

trait Message {
    function log($msg) {
        echo 'There is a message after running the class <em>' . __CLASS__ . '</em> at <em>' . date('Y-m-d h:i:s') . '</em>: ' . $msg . '<br>';
    }
}

class DBManager {
    use Message;
    function __construct($dbName) {
        $this->log('The Database (' . $dbName . ') Created.');
    }
}

class FileManager {
    use Message;
    function __construct($fileName) {
        $this->log('The file (' . $fileName . ') Created.');
    }
}

$db = new DBManager('baversion');
$file = new FileManager('baversion.txt');

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

استفاده از چندین trait در کلاس

هر کلاس می‌تواند از چندین trait استفاده کند:

trait DBManager {
    public function connect()
    {
        echo 'Successfully connected to the database.<br>';
    }

    public function query()
    {
        echo 'Query has executed successfully.<br>';
    }

    public function closeDB()
    {
        echo 'Connection to the database has been closed.<br>';
    }
}

trait FileManager {
    public function openFile()
    {
        echo 'Successfully opened the file.<br>';
    }

    public function save()
    {
        echo 'Your data saved in the file.<br>';
    }

    public function closeFile()
    {
        echo 'Connection handler of the file has been closed.<br>';
    }
}

trait Share {
    public function twitter() {
        echo 'Shared to twitter.<br>';
    }

    public function facebook() {
        echo 'Shared to facebook.<br>';
    }
}

trait SendMessage {
    public function email() {
        echo 'An email sent to admin.<br>';
    }
}
 
trait Log {
    function log() {
        return 'Log recorded in the log file.<br>';
    }
}
 
class Operation {
    use DBManager, FileManager, SendMessage, Share, Log;
 
    function execute() {
        $this->connect();
        $this->query();
        $this->closeDB();
        $this->openFile();
        $this->save();
        $this->closeFile();
        $this->twitter();
        $this->facebook();
        $this->email();
 
        echo $this->log();
    }
}
 
$operation = new Operation();
$operation->execute();

در مثال بالا از چندین trait در کلاس استفاده شده و متد execute، متدهای مشخص شده را به ترتیب اجرا می‌کند و به طور کلی چندین قدم زیر را انجام می‌دهد:

  1. ابتدا اتصال به دیتابیس ایجاد شده و کوئری اجرا می‌شود، سپس اتصال بسته می‌شود.
  2. دستگیره فایل باز می‌شود و مقداری در آن ذخیره می‌شود و دستگیره فایل بسته می‌شود.
  3. اطلاعات در شبکه‌های اجتماعی به اشتراک گذاشته می‌شود.
  4. ایمیلی برای مدیر ارسال می‌شود.
  5. در نهایت نتیجه اجرای عملیات نمایش داده می‌شود.

ساختن traitهای چندگانه

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

trait Scanner {
    public function scan() {
        echo 'File scanned.<br>';
    }
    publiv function save() {
        echo 'File saved.<br>';
    }
}
 
trait Printer {
    public function print(){
        echo 'File Printed.<br>';
    }
}
 
trait Copier {
    use Scanner, Printer;
    public function copy() {
        $this->scan();
        $this->save();
        $this->print();
    }
}

می‌بینید که هر trait می‌تواند از چندین trait دیگر استفاده کند.

نام مستعار برای traitها

اگر trait دارای متدی همنام با متد والد کلاس باشد، متد والد توسط متد داخل trait بازنویسی می‌شود و دلیل آن نیز این است که عمل ارث‌بری هنگام تعریف کلاس بوده ولی عمل استفاده از trait در بدنه کلاس. البته نگران نباشید، کماکان با کلمه کلیدی parent و عملگر :: می‌توانید به متد والد نیز دسترسی داشته باشید. اگر در مثال مربوط به استفاده از چندین trait در یک کلاس، متد بستن دیتابیس و متد بستن هندلر فایل که توسط دو trait متفاوت ساخته شده بودند، هر دو دارای نامی مشابه بودند، اینکار باعث ناسازگای و بروز خطا می‌گردد، برای رفع این مشکل می‌توان برای هر متد trait، یک نام مستعار تعیین کرد:

trait DBManager {
    public function close() {
        echo 'The method <em>close</em> called from DBManager<br>';
    }
}

trait FileManager {
    public function close() {
        echo 'The method <em>close</em> called from FileManager<br>';
    }
}

class Manager {
    use FileManager, DBManager {
        DBManager::close as closeDB;
        FileManager::close insteadof DBManager;
    }
}
 
$manager = new Manager();
$manager->closeDB();
$manager->close();

در مثال بالا با استفاده از کلمه کلیدی as به متد trait نام مستعار نسبت دادیم. همچنین با استفاده از insteadof تعیین کردیم که متد FileManager به جای DBManager با نام close فراخوانی شود.

متدهای انتزاعی در traitها

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

trait car {
    public function calculateFuel() {
        echo 'Feul claculated.';
    }
    abstract public function getFeulType();
}

دقت کنید که traitها کلاس نیستند، بلکه در واقع وسیله‌ای برای گروه‌بندی متدها هستند. پس نمی‌توانید در کد برنامه به صورت مستقیم از آن‌ها استفاده کنید و باید در سایر کلاس‌ها یا traitها از آن‌ها استفاده کنید.

پیمایشگر (Iterator)

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

پیمایشگر iterator چیست؟

iterate به معنی تکرار کردن است و منظور تکرار کردن عملی روی آیتم‌ها است که به خوبی در مورد حلقه foreach این مورد را می‌توانید ببینید. کتابخانه استاندارد پی‌اچ‌پی (SPL) مجموعه‌ای از iteratorها را برای پیمایش اشیا ارائه می‌دهد. اگر داده‌های شما زیاد باشد و بخواهید آن‌ها را هر بار در آرایه‌ها کپی نموده و آرایه‌ها را بااستفاده از حلقه foreach پیمایش کنید، مطلوب نیست، فراموش نکنید که دستورات داخلی پی‌اچ‌پی سرعت اجرای بسیار بیشتری نسبت به کدهای شما دارند، کار پیمایش آن‌ها را به iteratorها بسپارید.

پیمایشگر ArrayIterator

$user = array(
    'firstName' => 'Mojtaba',
    'lastName'  => 'Pakzad',
    'grade'     => 20,
);

$iteratorObject = new ArrayIterator($user);

foreach ($iteratorObject as $key => $value)
{
    echo $key . ' :  ' . $value . '<br>';
}

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

پیمایشگر RecursiveArrayIterator

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

$users = array(
    array('firstName' => 'Mojtaba', 'lastName' => 'Pakzad', 'email' => 'info@baversion.com'),
    array('firstName' => 'Milad', 'lastName' => 'Pakzad', 'email' => 'milad@baversion.com'),
    array('firstName' => 'Amir Ali', 'lastName' => 'Pakzad', 'email' => 'amirali@baversion.com'),
    'Account of paniya deleted.',
    'A user register but not verified yet.'
);

foreach ($users as $key => $value) 
{
    if (is_array($value)) 
    {
        foreach ($value as $k => $v) 
        {
            echo $k . ': ' . $v . '<br>';
        }
    }
    else
    {
        echo $key . ': ' . $value . '<br>';
    }
}

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

$users = array(
    array('firstName' => 'Mojtaba', 'lastName' => 'Pakzad', 'email' => 'info@baversion.com'),
    array('firstName' => 'Milad', 'lastName' => 'Pakzad', 'email' => 'milad@baversion.com'),
    array('firstName' => 'Amir Ali', 'lastName' => 'Pakzad', 'email' => 'amirali@baversion.com'),
    'Account of paniya deleted.',
    'A user register but not verified yet.'
);

$iteratorObject = new RecursiveArrayIterator($users);

foreach(new RecursiveIteratorIterator($iteratorObject) as $key => $value) {
    echo $key . ": " . $value . "<br>";
}

رابط Iterator

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

  • متد current: عنصر جاری را برمی‌گرداند.
  • متد key: اندیس عنصر جاری را برمی‌گرداند.
  • متد next: اشاره‌گر را به خانه بعدی آرایه می‌برد.
  • متد rewind: پیمایشگر را ریست می‌کند و به خانه اول می‌برد.
  • متد valid: بررسی می‌کند که آیا موقعیت جاری عنصر ولید است.

ترتیب اجرای متدهای بالا در هنگام پیمایش در یک حلقه foreach به صورت زیر است:

  1. قبل از پیمایش حلقه ابتدا متد rewind فراخوانی می‌شود.
  2. در ابتدای هر حلقه متد valid فراخوانی می‌شود که یک مقدار بولین برمی‌گرداند.
  3. اگر متد valid مقدار true برگرداند، به ترتیب متدهای current و key اجرا می‌شوند و اگر false برگرداند، حلقه به پایان می‌رسد.
  4. بدنه حلقه اجرا می‌شود.
  5. بعد از هر پیمایشی که توسط حلقه انجام می‌گیرد، متد next فراخوانی می‌شود و سپس پیمایش از گام 2 دوباره تکرار می‌شود.
class Animal implements Iterator
{
    protected $position = 0;

    protected $pets = array('Cat', 'Dog', 'Bird', 'Fish');

    public function rewind() {
        echo 'Loop started using method <em>rewind</em><br>';
        $this->position = 0;
    }

    public function current() {
        echo 'method <em>current</em> called after that ';
        return $this->pets[$this->position];
    }

    public function key() {
        echo 'method <em>key</em> called.<br>';
        return $this->position;
    }

    public function next() {
        echo 'Method <em>next</em> called to go to next index of array.<hr>';
        ++$this->position;
    }

    public function valid() {
        echo 'Validation using method <em>valid</em>: ';
        if(isset($this->pets[$this->position]))
        {
            echo 'is valid then ';
            return true;
        }
        else
        {
            echo 'isn\'t valid and Loop finished.';
            return false;
        }
    }
}

$animal = new Animal();
foreach ($animal as $key => $value) {
    echo '<strong>Now:</strong> index of array is <em>' . $key . '</em> and value of it is <em>' . $value . '</em><br>';
}

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

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

پیمایشگر ArrayObject

این متد نیز برای پیمایش است و دارای 5 متد است:

  • متد append: این متد هر مقداری را به پایان مجموعه اضافه می‌کند.
  • متد getIterator: این متد یک شی Iterator می‌سازد و آن را برمی‌گرداند. به همین ترتیب شما می‌توانید پیمایش را با کمک سبک Iteration انجام دهید. این یک متد بسیار مفید برای گرفتن یک شی Iterator از یک آرایه است.
  • متد offsetExists: این متد می‌تواند مشخص کند که آیا offset خاصی، در مجموعه وجود دارد.
  • متد offsetGet: این متد مقدار offset مشخص شده را برمی‌گرداند.
  • متد offsetSet: این متد می‌تواند هر مقداری را برای اندیس مشخص شده قرار دهد.
  • متد offsetUnset: این متد می‌تواند عناصر مقداردهی نشده را در اندیس خاصی قرار دهد.
نکته: متد append فقط یک مقدار می گیرد و اندیس را به صورت خودکار و عددی قرار می دهد، برای اندیس انجمنی باید از offsetSet استفاده کنیم.

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

جمع‌بندی

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

تگ‌ها
اشتراک‌گذاری