اذهب إلى المحتوى

الأصناف (Classes) والكائنات (Objects) في PHP


سارة محمد2

تستخدم الأصناف والكائنات لجعل الشيفرة أكثر فعالية وأقل تكرارًا عن طريق تجميع الوظائف المتشابهة.

يستخدم الصنف لتعريف الوظائف وبنى المعطيات المستخدمة لإنشاء كائنات، ثم تُنشأ الكائنات باستخدام البنية المعرّفة مسبقًا.

ثوابت الصنف

توفر ثوابت الصنف (Class constants) آلية لحمل القيم الثابتة في البرنامج، أي أنّها توفر طريقة لإعطاء اسم (وتربطه بالتحقق وقت التصريف) لقيمة ما مثل 3.14 أو "Apple"، يمكن أن تُعرَّف ثوابت الصنف باستخدام الكلمة المفتاحية const فقط ولا يمكن استخدام الدالة define لهذا الغرض.

فمثلًا قد يكون من المناسب أن يكون لديك تمثيل مختصر للقيمة π في برنامجك، إليك الصنف التالي مع قيم const يوفر طريقة بسيطة لحمل مثل هذه القيم.

class MathValues {
    const PI = M_PI;
    const PHI = 1.61803;
}

$area = MathValues::PI * $radius * $radius;

يمكن الوصول لثوابت الصنف مثل المتغيرات الساكنة باستخدام عامل العمود المزدوج :: (الذي يسمى عامل دقة النطاق) على الصنف، لكن على عكس المتغيرات الساكنة فإنّ متغيرات الصنف لها قيم ثابتة وقت التصريف ولا يمكن إعادة إسناد قيم لها، (أي أنّ إضافة MathValues::PI = 7 للشيفرة السابقة ستنتج خطأً فادحًا).

تفيد ثوابت الصنف أيضًا في تعريف الأشياء الداخلية للصنف التي قد تحتاج للتغيير لاحقًا (لكن لا تتغير بشكلٍ مستمر بما يكفي لتخزينها في قاعدة بيانات مثلًا)، يمكن الرجوع إليها داخليًا باستخدام عامل تحليل النطاق (Scope Resolution Operator) ‏self الذي يعمل في التنفيذات الساكنة والمستنسخة.

class Labor {
    // الوقت اللازم لبناء عنصر مقدّرًا بالساعات
    const LABOR_UNITS = 0.26;

    // المبلغ الذي يتقاضاه العمّال في الساعة
    const LABOR_COST = 12.75;

    public function getLaborCost($number_units) {
        return (self::LABOR_UNITS * self::LABOR_COST) * $number_units;
    }
}

يمكن أن تحتوي ثوابت الصنف في إصدارات PHP السابقة للإصدار 5.6 على قيم عددية فقط، بينما يمكننا استخدام التعابير مع الثوابت بدءًا من الإصدار 5.6 أي أنّه يمكننا استخدام التعليمات الرياضية والسلاسل النصية مع الربط.

class Labor {

    /** ندفع للعمال في الساعة: الأجر في الساعة * عدد الساعات لإنجاز العمل */
    const LABOR_COSTS = 12.75 * 0.26;

    public function getLaborCost($number_units) {
        return self::LABOR_COSTS * $number_units;
    }
}

بدءًا من الإصدار PHP 7.0، يمكن للثوابت المعرّفة باستخدام define أن تحتوي مصفوفات.

define("BAZ", array('baz'));

تعدّ ثوابت الصنف مفيدة ليس فقط لتخزين المفاهيم الرياضية، فمثلًا إذا كنت تحضّر شطيرة قد يكون من المناسب أن يكون لديك صنف Pie واحد قادر على أخذ أنواع مختلفة من الفاكهة.

class Pie {
    protected $fruit;

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

بعدها يمكننا استخدام الصنف Pie كالتالي:

$pie = new Pie("strawberry");

المشكلة التي تظهر هنا هي أنّه عند استنساخ الصنف Pie لا يوجد إرشادات للقيم المقبولة، فمثلًا عند صنع شطيرة "boysenberry" قد تُكتب مع خطأ إملائي "boisenberry"، بالإضافة إلى ذلك قد لا تريد دعم صنع شطيرة خوخ. لذا سيكون من المفيد أن يكون لدينا قائمة معرّفة مسبقًا تحوي أنواع الفاكهة المقبولة موجودة في مكان ما بحيث يمكن ملاحظتها وليكن صنف نسميه Fruit:

class Fruit {
    const APPLE = "apple";
    const STRAWBERRY = "strawberry";
    const BOYSENBERRY = "boysenberry";
}

$pie = new Pie(Fruit::STRAWBERRY);

يوفر وضع القيم المقبولة في قائمة ثوابت الصنف دليلًا للقيم التي يقبلها التابع، كما يضمن أن لا يقبل المصرّف الأخطاء الإملائية التي قد تحدث، فبينما يقبل المصرّف new Pie('aple')‎ وnew Pie('apple')‎ ستنتج الشيفرة new Pie(Fruit::APLE)‎ خطأ تصريف (compiler error).

كما إنّ استخدام ثوابت الصنف يعني أنّ القيمة الفعلية للثابت يمكن تعديلها في مكان واحد وأيّ شيفرة تستخدم هذا الثابت ستتأثر بهذا التغيير تلقائيًا.

إنّ الطريقة الشائعة للوصول إلى ثابت الصنف هي MyClass::CONSTANT_NAME، لكن يمكن الوصول إليها بالطريقة التالية:

echo MyClass::CONSTANT;
$classname = "MyClass";

// PHP 5.3.0 بدءًا من
echo $classname::CONSTANT; 

تسمى عادةً ثوابت الصنف بأحرف كبيرة مع استخدام الشرطة السفلية _ للفصل بين الكلمات، بالرغم من أنّه يمكن استخدام أي تسمية صحيحة لتسمية ثابت الصنف.

بدءًا من الإصدار PHP 7.1 يمكن تعريف ثوابت الصنف بمرئيات مختلفة عن النطاق العام الافتراضي، أي يمكن تعريف ثوابت صنف خاصة (private) ومرئية (protected) لمنعها من التسرّب غير الضروري إلى النطاق العام، مثال:

class Something {
    const PUBLIC_CONST_A = 1;
    public const PUBLIC_CONST_B = 2;
    protected const PROTECTED_CONST = 3;
    private const PRIVATE_CONST = 4;
}

define مقابل ثوابت الصنف

بالرغم من أنّ هذه البنية صحيحة:

function bar() { return 2; };
define('BAR', bar());

لكن سيظهر خطأ إذا حاولت القيام بذلك مع ثوابت الصنف:

function bar() { return 2; };

class Foo {
    const BAR = bar(); 
    // Constant expression contains invalid operations
}

لكن يمكنك كتابة التالي:

function bar() { return 2; };

define('BAR', bar());

class Foo {
    const BAR = BAR;
}

استخدام ‎::class‎ للحصول على اسم الصنف

قدمت PHP 5.5 الصياغة ‎::class للحصول على اسم الصنف الكامل?? مع مراعاة نطاق فضاء الاسم وتعليمات use.

namespace foo;
use bar\Bar;
echo json_encode(Bar::class); // "bar\\Bar"
echo json_encode(Foo::class); // "foo\\Foo"
echo json_encode(\Foo::class); // "Foo"

تعمل الشيفرة السابقة بشكلٍ صحيح حتى لو لم تُعرَّف الأصناف، وتعدّ هذه الصياغة مفيدة للدوال التي تتطلّب اسم صنف فيمكن استخدامها مع الدالة class_exists مثلًا للتحقق من أنّ صنف ما معرّف. لن تظهر أخطاء في الشيفرة التالية بغض النظر عن القيمة المعادة:

class_exists(ThisClass\Will\NeverBe\Loaded::class, false);

الأصناف المجردة

الصنف المجرد (abstract class) هو الصنف الذي لا يمكن استنساخه، ويمكن له أن يعرّف توابعًا مجردة أي توابع لها تعريف فقط من دون جسم:

abstract class MyAbstractClass {
    abstract public function doSomething($a, $b);
}

يمكن أن توسَّع الأصناف المجردة بصنف ابن يحقق تنفيذ هذه التوابع المجردة، إذ أنّ الهدف الأساسي للصنف المجرد هو تعريف قالب يسمح للأصناف الأبناء بالوراثة منه وفق بنية معينة، إليك المثال التالي الذي ننفذ فيه واجهة Worker بعد تعريفها:

interface Worker {
    public function run();
}

لتسهيل تطوير المزيد من تطبيقات الواجهة Worker ننشئ الصنف المجرد worker المزود بالتابع ‎run()‎ من الواجهة ويحدد بعض التوابع المجردة لتُملأ في الأصناف الأبناء:

abstract class AbstractWorker implements Worker {
    protected $pdo;
    protected $logger;

    public function __construct(PDO $pdo, Logger $logger) {
        $this->pdo = $pdo;
        $this->logger = $logger;
    }

    public function run() {
        try {
            $this->setMemoryLimit($this->getMemoryLimit());
            $this->logger->log("Preparing main");
            $this->prepareMain();
            $this->logger->log("Executing main");
            $this->main();
        } catch (Throwable $e) {
            // تلتقط وتعيد رمي كل الأخطاء حتى يتمكن العامل من تسجيلها
            $this->logger->log("Worker failed with exception: {$e->getMessage()}");
            throw $e;
        }
    }

    private function setMemoryLimit($memoryLimit) {
        ini_set('memory_limit', $memoryLimit);
        $this->logger->log("Set memory limit to $memoryLimit");
    }

    abstract protected function getMemoryLimit();

    abstract protected function prepareMain();

    abstract protected function main();
}

وضعنا في البداية تابعًا مجردًا getMemoryLimit()‎، يحتاج أي صنف يرث الصنف AbstractWorker أن ينفذ هذا التابع ويعيد قيد الذاكرة. ثم يضبط AbstractWorker قيد الذاكرة ويسجّله.

يستدعي الصنف AbstractWorker بعد ذلك التوابع prepareMain()‎ وmain()‎، ثم تُجمّع كل استدعاءات التوابع هذه في كتلة try-catch فإذا رمى أي من التوابع المجردة المعرفة من الصنف الابن استثناءً سنلتقط هذا الاستثناء ونسجّله ونعيد رميه، وهذا يمنع كل الأصناف الأبناء من الاضطرار إلى تنفيذ هذا الإجراء.

لنعرّف صنف ابن يوسّع الصنف AbstractWorker:

class TranscactionProcessorWorker extends AbstractWorker {
    private $transactions;

    protected function getMemoryLimit() {
        return "512M";
    }

    protected function prepareMain() {
        $stmt = $this->pdo->query("SELECT * FROM transactions WHERE processed = 0 LIMIT 500");
        $stmt->execute();
        $this->transactions = $stmt->fetchAll();
    }

    protected function main() {
        foreach ($this->transactions as $transaction) {
        // (1)
        $stmt = $this->pdo->query("UPDATE transactions SET processed = 1 WHERE id ={$transaction['id']} LIMIT 1");
        $stmt->execute();
        }
    }
}

قد يُرمى استثناء PDO أو MYSQL في الموضع (1) لكن يُعالج من قبل AbstractWorker.

كما تلاحظ فإنّ TransactionProcessorWorker كان سهل التنفيذ حيث كان علينا فقط تحديد قيد الذاكرة والقلق بشأن الإجراءات الفعلية التي نحتاج القيام بها. لا نحتاج إلى معالجة الأخطاء في TransactionProcessorWorker لأنّها تُعالج في الصنف AbstractWorker.

ملاحظة مهمة: عند الوراثة من صنف مجرد فإنّ كل التوابع المجردة الموجودة في الصنف الأب يجب أن يعرّفها الابن (أو يعيد التصريح عنها بأنّها توابع مجردة)، كما يجب تعريف هذه التوابع بنفس المرئية (أو بمرئية أقل تحديدًا)، فمثلًا إذا عُرِّف التابع المجرد أنه محمي فإنّ تنفيذه يجب أن يعرّف أنّه محمي أو عام (ولا يمكن تعريفه أنّه خاص).

سيُرمى خطأً فادحًا إذا لم تعرّف توابع الأصناف الآباء المجردة في الصنف الابن كالخطأ التالي:

Fatal error: Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (X::x) in

الربط الساكن المتأخر

يمكن استخدام الربط الساكن المتأخر بدءًا من الإصدار PHP 5.3 للتحكم في الصنف الذي اُستدعيت منه خاصيّة ساكنة أو تابع ساكن. وأُضيف للتغلب على مشكلة عامل تحليل النطاق ‎self::‎، إليك الشيفرة التالية:

class Horse {
    public static function whatToSay() {
        echo 'Neigh!';
    }

    public static function speak() {
        self::whatToSay();
    }
}

class MrEd extends Horse {
    public static function whatToSay() {
        echo 'Hello Wilbur!';
    }
}

قد تتوقع أنّ الصنف MrEd سيعيد تعريف الدالة whatToSay()‎ في الصنف الأب لكن عند التنفيذ ستكون النتيجة غير متوقعة:

Horse::speak(); // Neigh!
MrEd::speak(); // Neigh!

المشكلة هي أنّ self::whatToSay();‎ يمكن أن يشير إلى الصنف Horse فقط أي أنّه لن يتأثر بالصنف MrEd، لن نواجه هذه المشكلة إذا بدلنا إلى عامل تحليل النطاق static::‎، وبهذه الطريقة سيتأثر الصنف بالنسخة التي تستدعيه فنحصل على الوراثة التي نتوقعها:

class Horse {
    public static function whatToSay() {
        echo 'Neigh!';
    }

    public static function speak() {
        // ربط ساكن متأخر
        static::whatToSay();
    }
}

Horse::speak(); // Neigh!
MrEd::speak(); // Hello Wilbur!

فضاء التسمية والتحميل التلقائي

يعمل التحميل التلقائي (Autoloading) تقنيًا بتنفيذ رد نداء عندما يُطلب صنف PHP ولا يُعثر عليه، عادةً ما تحاول ردود النداء تحميل هذه الأصناف.

يمكن أن يُفهم التحميل التلقائي بشكلٍ عام أنّه محاولة لتحميل ملفات PHP (خاصةً ملفات الصنف حيث يكون الملف المصدري مخصصًا لصنف معين) من المسارات المناسبة حسب الاسم الكامل المؤهل (FQN) عند الحاجة إلى الصنف.

بفرض لدينا الأصناف التالية: ملف الصنف application\controllers\Base:

<?php
namespace application\controllers { class Base {...} }

ملف الصنف application\controllers\Control:

<?php
namespace application\controllers { class Control {...} }

ملف الصنف application\models\Page:

<?php
namespace application\models { class Page {...} }

تتوضع هذه الأصناف في المجلد المصدر وفقًا لأسمائها الكاملة المؤهلة كالتالي:

  • المجلد المصدر
    • applications
      • controllers
        • Base.php
        • Control.php
      • models
        • Page.php

تجعل هذه الطريقة من الممكن الحصول على مسار ملف الصنف برمجيًا وفق الاسم الكامل المؤهل (FQN) باستخدام هذه الدالة:

function getClassPath(string $sourceFolder, string $className, string $extension = ".php") {
    return $sourceFolder . "/" . str_replace("\\", "/", $className) . $extension;
}

لاحظ أنّ / هو الفاصل بين المجلدات في ويندوز.

تسمح لنا الدالة spl_autoload_register بتحميل الصنف الذي نحتاجه باستخدام دالة معرفة من قبل المستخدم:

const SOURCE_FOLDER = __DIR__ . "/src";

spl_autoload_register(function (string $className) {
    $file = getClassPath(SOURCE_FOLDER, $className);
    if (is_readable($file)) require_once $file;
});

يمكن لهذه الدالة أن توسَّع لاستخدام توابع قيم تراجعية (fallback) للتحميل:

const SOURCE_FOLDERS = [__DIR__ . "/src", "/root/src"]);
spl_autoload_register(function (string $className) {
    foreach(SOURCE_FOLDERS as $folder) {
        $extensions = [
            // ؟src/Foo/Bar.php5_int64 هل يوجد لدينا
            ".php" . PHP_MAJOR_VERSION . "_int" . (PHP_INT_SIZE * 8),

            // ؟src/Foo/Bar.php7 هل يوجد لدينا
            ".php" . PHP_MAJOR_VERSION,

            // ؟src/Foo/Bar.php_int64 هل يوجد لدينا
            ".php" . "_int" . (PHP_INT_SIZE * 8),

            // ؟src/Foo/Bar.phps هل يوجد لدينا
            ".phps"

            // ؟src/Foo/Bar.php هل يوجد لدينا
            ".php"
        ];

        foreach($extensions as $ext) {
            $path = getClassPath($folder, $className, $extension);
            if(is_readable($path)) return $path;
        }
    }
});

لاحظ أنّ PHP لا تحاول تحميل الصنف كلما حُمِّل ملف يستخدمه، فقد تحمّله في وسط السكربت أو عند توقف الدوال وهذا إحدى الأسباب التي تجعل المطورين، خاصة الذين يستخدمون التحميل التلقائي، يتجنبوا استبدال ملفات المصدر التنفيذية في وقت التنفيذ خاصةً في ملفات phar.

مرئية التوابع والخاصيّات

لدينا ثلاث أنواع للمرئية يمكن استخدامها للتوابع (دوال الصنف/الكائن) والخاصيّات (متغيرات الصنف/الكائن) داخل الصنف مما يوفر تحكم بالوصول لهذه التوابع والخاصيّات.

public: المرئية العامة

يعني التصريح عن تابع أو خاصيّة بأنّهما public أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما والأصناف التي ترثه وأيّ كائنات أو أصناف خارجية أو شيفرة خارج هرمية الصنف، مثال:

class MyClass {
    // خاصيّة
    public $myProperty = 'test';

    // تابع
    public function myMethod() {
        return $this->myProperty;
    }
}

$obj = new MyClass();
echo $obj->myMethod();
// test

echo $obj->myProperty;
// test

Protected: المرئية المحمية

يعني التصريح عن تابع أو خاصيّة بأنّهما protected أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما والأصناف التي ترثه، ولا يمكن الوصول إليهما من الكائنات أو الأصناف الخارجية أو الشيفرة خارج هرمية الصنف. إذا استخدمتهما شيفرة لا تملك إمكانية الوصول إليهما سيُرمى خطأ. مثال:

class MyClass {
    protected $myProperty = 'test';
    protected function myMethod() {
        return $this->myProperty;
    }
}

class MySubClass extends MyClass {
    public function run() {
        echo $this->myMethod();
    }
}

$obj = new MySubClass();
// MyClass::myMethod(); يستدعي السطر التالي
$obj->run(); 
// test

$obj->myMethod(); // خطأ
// Fatal error: Call to protected method MyClass::myMethod() from context ''

يوضح المثال السابق أنّه يمكن الوصول إلى العناصر المحمية من ضمن نطاقها، أي "الذي يوجد في المنزل يمكن الوصول إليه من داخل المنزل فقط".

Private: المرئية الخاصة

يعني التصريح عن تابع أو خاصيّة بأنّهما private أنّه يمكن الوصول إليهما من الصنف الذي صرّح عنهما فقط (ولا يمكن الوصول إليهما من الأصناف الفرعية).

لاحظ أنّ الكائنات من نفس النوع ستصل إلى العناصر الخاصة والمحمية لبعضها لبعض حتى لو لم تكن نفس النسخ.

class MyClass {
    private $myProperty = 'test';

    private function myPrivateMethod() {
        return $this->myProperty;
    }

    public function myPublicMethod() {
        return $this->myPrivateMethod();
    }

    public function modifyPrivatePropertyOf(MyClass $anotherInstance) {
        $anotherInstance->myProperty = "new value";
    }
}

class MySubClass extends MyClass {
    public function run() {
        echo $this->myPublicMethod();
    }

    public function runWithPrivate() {
        echo $this->myPrivateMethod();
    }
}

$obj = new MySubClass();
$newObj = new MySubClass();

$obj->run(); // (1)
// test

$obj->modifyPrivatePropertyOf($newObj);

$newObj->run();
// new value

echo $obj->myPrivateMethod(); // خطأ
// Fatal error: Call to private method MyClass::myPrivateMethod() from context ''

echo $obj->runWithPrivate(); // خطأ
// Fatal error: Call to private method MyClass::myPrivateMethod() from context 'MySubClass'

في الموضع (1) سيُستدعى MyClass::myPublicMethod()‎ ثم MyClass::myPrivateMethod()‎.

كما تلاحظ يمكن الوصول إلى التابع/الخاصيّة الخاصة فقط من الصنف الذي عرفهما.

الواجهات

الواجهات (Interfaces) هي تعريفات لواجهة برمجة التطبيقات (API) العامة ويجب أن تنفذها الأصناف، فهي تعمل "كعقود" تحدد ما الذي يجب أن تفعله الأصناف دون تحديد كيفية فعله.

يشبه تعريف الواجهة تعريف الصنف لكن نستبدل كلمة interface بكلمة class:

interface Foo {
}

يمكن أن تحتوي الواجهات توابع و/أو ثوابت ولكن ليس سمات، ثوابت الواجهة لها نفس قيود ثوابت الصنف، وتوابع الواجهة مجردة ضمنيًّا:

interface Foo {
    const BAR = 'BAR';

    public function doSomething($param1, $param2);
}

ملاحظة: يجب ألا تصرّح الواجهات عن بواني (constructors) أو هوادم (destructors) لأن تفاصيل التنفيذ هذه على مستوى الصنف.

التطبيق

أي صنف يحتاج لتنفيذ واجهة يجب أن يستخدم الكلمة المفتاحية implements، للقيام بذلك يحتاج الصنف أن يوفر تنفيذًا لكل تابع مصرَّح عنه في الواجهة مع الحفاظ على نفس البصمة، ويمكن للصنف الواحد أن ينفذ أكثر من واجهة واحدة في نفس الوقت.

interface Foo {
    public function doSomething($param1, $param2);
}

interface Bar {
    public function doAnotherThing($param1);
}

class Baz implements Foo, Bar {
    public function doSomething($param1, $param2) {
        // ...
    }

    public function doAnotherThing($param1) {
        // ...
    }
}

لا تحتاج الأصناف المجردة عندما تنفذ الواجهات إلى تنفيذ كل التوابع، وأي تابع لا يُنفَّذ في الصنف الأساسي يجب تنفيذه في الصنف الذي يوسّعه:

abstract class AbstractBaz implements Foo, Bar {
    // تنفيذ جزئي للواجهة المطلوبة...
    public function doSomething($param1, $param2) {
        // ...
    }
}

class Baz extends AbstractBaz {
    public function doAnotherThing($param1) {
        // ...
    }
}

لاحظ أنّ تطبيق الواجهة ميزة موروثة، عند توسيع صنف ينفذ واجهة فأنت لا تحتاج لإعادة التصريح عنها في الصنف الابن لأنّ هذا محقق ضمنيًّا.

ملاحظة: قبل الإصدار PHP 5.3.9 لم يكن بإمكان الصنف أن ينفذ واجهتين بهما تابع بنفس الاسم لأنّ هذا قد يسبب التباسًا، بينما تسمح الإصدارات الأحدث بذلك طالما أنّ التوابع المكررة ليس لها نفس البصمة.

الوراثة

يمكن إنشاء علاقة وراثة بين الواجهات مثل الأصناف باستخدام الكلمة المفتاحية extends، الفرق الأساسي هو أنّ الوراثة المتعددة مسموحة في الواجهات:

interface Foo {
}

interface Bar {
}

interface Baz extends Foo, Bar {
}

أمثلة

إليك مثال بسيط لواجهة مركبة، يمكن للمركبات أن تتحرك إلى الأمام والخلف.

interface VehicleInterface {
    public function forward();
    public function reverse();
    ...
}

class Bike implements VehicleInterface {
    public function forward() {
        $this->pedal();
    }

    public function reverse() {
        $this->backwardSteps();
    }

    protected function pedal() {
        ...
    }

    protected function backwardSteps() {
        ...
    }
    ...
}

class Car implements VehicleInterface {
    protected $gear = 'N';

    public function forward() {
        $this->setGear(1);
        $this->pushPedal();
    }

    public function reverse() {
        $this->setGear('R');
        $this->pushPedal();
    }

    protected function setGear($gear) {
        $this->gear = $gear;
    }

    protected function pushPedal() {
        ...
    }
    ...
}

ثم ننشئ صنفين ينفذان الواجهة: الدراجة والسيارة، يوجد اختلاف كبير داخليًا بين الدراجة والسيارة لكن كلّ منهما مركبة ويجب أن تنفذ نفس التوابع العامة التي توفرها واجهة المركبة VehicleInterface.

تسمح ميزة التلميح عن النوع (Typehinting) للتوابع والدوال أن تطلب واجهات. بفرض أنّه لدينا صنف مرآب للسيارات يحتوي كل أنواع المركبات.

class ParkingGarage {
    protected $vehicles = [];

    public function addVehicle(VehicleInterface $vehicle) {
        $this->vehicles[] = $vehicle;
    }
}

بما أنّ الدالة addVehicle تتطلب متغيرًا ‎$vehicle من النوع واجهة VehicleInterface - ليس تنفيذ حقيقي - يمكننا أن نجعل الدخل من النوع دراجة أو سيارة مما يمكّن الصنف ParkingGarage من معالجتها واستخدامها.

الكلمة المفتاحية Final

تمنع الكلمة المفتاحية Final الأصناف الأبناء من إعادة تعريف تابع مسبوقة بالكلمة final، إذا عُرِّف الصنف أنّه نهائي (final) فلا يمكن توسيعه.

التابع النهائي

class BaseClass {
    public function test() {
        echo "BaseClass::test() called\n";
    }

    final public function moreTesting() {
        echo "BaseClass::moreTesting() called\n";
    }
}

class ChildClass extends BaseClass {
    public function moreTesting() {
        echo "ChildClass::moreTesting() called\n";
    }
}

// Fatal error: Cannot override final method BaseClass::moreTesting()

الصنف النهائي

final class BaseClass {
    public function test() {
        echo "BaseClass::test() called\n";
    }

    // من غير الهام هنا إن عُرِّفت الدالة أنّها نهائية أو لم تُعرَّف
    final public function moreTesting() {
        echo "BaseClass::moreTesting() called\n";
    }
}

class ChildClass extends BaseClass {
}
// Fatal error: Class ChildClass may not inherit from final class (BaseClass)

الثوابت النهائية

لا تستخدم الكلمة المفتاحية final لثوابت الصنف في PHP، على عكس لغة Java. ويمكن استخدام الكلمة المفتاحية const بدلًا منها.

لماذا يجب أن أستخدم الكلمة المفتاحية final؟

  • منع حدوث سلسلة وراثة ضخمة.
  • تشجيع التكوّن
  • إجبار المطور على التفكير بواجهة برمجة التطبيقات العامة للمستخدم
  • إجبار المطور على تقليص واجهة برمجة التطبيقات العامة للكائن
  • يمكن أن نجعل الصف المعرّف أنّه final موسِّعًا
  • تحطم الكلمة المفتاحية extends التغليف
  • لا تحتاج إلى المرونة
  • بإمكانك تغيير الشيفرة بحريّة

لماذا يجب أن أتجنب استخدام الكلمة المفتاحية final؟

تعمل الأصناف النهائية بشكلٍ فعال فقط في وجود الافتراضات التالية:

  • يوجد تجريد (واجهة) ينفذه الصنف النهائي
  • كل واجهة برمجة التطبيقات العامة للصنف النهائي هي جزء من تلك الواجهة

التحميل التلقائي

‏بالطبع أنت لا ترغب في استخدام include وrequire في كل مرة تريد استخدام صنف أو وراثته، لأنّ هذا قد يكون سيئًا وسهل النسيان لذا وفرت PHP التحميل التلقائي (autoloading).

ما هو التحميل التلقائي بالضبط؟

يعني التحميل التلقائي أنّك لا تحتاج للحصول على الملف المخزّن فيه الصنف المطلوب إنّما تقوم PHP "تلقائيًا" بتحميله.

كيف يمكن أن أقوم بالتحميل التلقائي في PHP دون شيفرة من طرف ثالث؟

يمكننا استخدام الدالة ‎_autoload‎_ للقيام بالتحميل التلقائي لكن يفضّل استخدام الدالة spl_autoload_register، تستخدم PHP هذه الدوال عندما لا تجد صنف ما معرفًا ضمن النطاق المعطى. إنّ إضافة التحميل التلقائي إلى مشروع موجود لا يشكل مشكلة بما أنّ الأصناف المعرّفة (باستخدام require) ستعمل كما في السابق. ستستخدم الأمثلة التالية دوال مجهولة فإذا كنت تستخدم إصدار PHP ‎‎< 5.3 يمكنك تعريف الدالة وتمرير اسمها كوسيط للدالة spl_autoload_register.

أمثلة

spl_autoload_register(function ($className) {
    $path = sprintf('%s.php', $className);
    if (file_exists($path)) {
        include $path;
    } else {
        // الملف غير موجود
    }
});

تحاول الشيفرة السابقة تضمين اسم الملف الذي هو نفس اسم الصنف ومضافًا إليه اللاحقة "‎.php" باستخدام sprintf، إذا احتجنا لتحميل FooBar فإنّها تبحث عن FooBar.php وتضمّنه. وبالطبع يمكن توسيع هذه الشيفرة لتناسب احتياجات مشروعنا، إذا اُستخدمت _ في اسم الصف للتجميع مثل User_Post وUser_Image فمن الممكن تخزين هذه الأصناف في مجلد اسمه "User" كالتالي:

spl_autoload_register(function ($className) {
    // استبدال بالمحرف / أو \ وذلك حسب نظام التشغيل
    $path = sprintf('%s.php', str_replace('_', DIRECTORY_SEPARATOR, $className) );
    if (file_exists($path)) {
        include $path;
    } else {
        // الملف غير موجود
    }
});

سيُحمّل الصنف User_Post الآن من المسار "User/Post.php" وهكذا.

يمكن أن تتناسب الدالة spl_autoload_register مع الاحتياجات المختلفة، فإذا كانت كل ملفات أصنافك مُسمّاة بالشكل "class.CLASSNAME.php" ستوجد تداخلات مختلفة (UserPostContent => "User/Post/Content.php"‎).

إذا كنت تريد تقنية تحميل تلقائي أكثر تفصيلًا ولا تريد استخدام composer يمكنك أن تعمل دون إضافة مكتبات من طرف ثالث.

spl_autoload_register(function ($className) {
    $path = sprintf('%1$s%2$s%3$s.php',
        realpath(dirname(__FILE__)),
        DIRECTORY_SEPARATOR,
        strtolower(
            // استبدال بالمحرف / أو \ وذلك حسب نظام التشغيل
            str_replace('_', DIRECTORY_SEPARATOR, $className)
        )
    );

    if (file_exists($path)) {
        include $path;
    } else {
        throw new Exception(
        sprintf('Class with name %1$s not found. Looked in %2$s.',
                $className,
                $path
            )
        );
    }
});

في الشيفرة السابقة ‎%1$s للحصول على المسار المطلق و‎%2$s إما / أو \ وذلك حسب نظام التشغيل و‎%3$s لعدم الاهتمام فيما إذا كانت الأحرف صغيرة أو كبيرة عند إنشاء الملفات.

باستخدام المحمّلات التلقائية مثل هذه يمكنك كتابة الشيفرة التالية:

// spl_autoload_register المكان الذي عُرِّف فيه
require_once './autoload.php'; 

$foo = new Foo_Bar(new Hello_World());

استخدام الأصناف:

class Foo_Bar extends Foo {}
class Hello_World implements Demo_Classes {}

ستضمّن هذه الأمثلة الأصناف من foo/bar.php، ‏foo.php، ‏hello/world.php وdemo/classes.php.

‎استدعاء باني أب عند استنساخ ابن

من المشاكل الشائعة للأصناف الأبناء هي أنّه إذا احتوى كل من الأب والابن تابعًا بانيًا ‎__construct()‎ فسيُنفَّذ باني الصنف الابن فقط، قد تحتاج أحيانًا إلى تنفيذ باني الصنف الأب من داخل الصنف الابن عندها يمكنك استخدام عامل تحليل النطاق ‎::parent بالشكل التالي:

parent::__construct();

إليك مثالًا واقعيًا:

class Foo {
    function __construct($args) {
        echo 'parent';
    }
}

class Bar extends Foo {
    function __construct($args) {
        parent::__construct($args);
    }
}

سيؤدي تنفيذ الشيفرة السابقة إلى تنفيذ باني الصنف الأب وبالتالي ستُنفَّذ تعليمة echo.

الربط الديناميكي

يعدّ الربط الديناميكي (ويسمىّ أيضًا إعادة تعريف التابع) مثالًا للتعددية الشكلية وقت التنفيذ والتي تحدث عندما تحتوي عدة أصناف على تنفيذات مختلفة لنفس التابع لكن الكائن الذي سيستدعيه التابع لا يُعرف حتى وقت التنفيذ، ويعد هذا مفيدًا إذا حدد شرط معين أيّ صنف سيُستخدم لأداء وظيفة ما حيث هذه الوظيفة لها نفس الاسم في كلا الصنفين.

interface Animal {
    public function makeNoise();
}

class Cat implements Animal {
    public function makeNoise
    {
        $this->meow();
    }
    ...
}

class Dog implements Animal {
    public function makeNoise {
        $this->bark();
    }
    ...
}

class Person {
    const CAT = 'cat';
    const DOG = 'dog';
    private $petPreference;
    private $pet;

    public function isCatLover(): bool {
        return $this->petPreference == self::CAT;
    }

    public function isDogLover(): bool {
        return $this->petPreference == self::DOG;
    }

    public function setPet(Animal $pet) {
        $this->pet = $pet;
    }

    public function getPet(): Animal {
        return $this->pet;
    }
}

if($person->isCatLover()) {
    $person->setPet(new Cat());
} else if($person->isDogLover()) {
    $person->setPet(new Dog());
}

$person->getPet()->makeNoise();

في المثال السابق لا يُعرف الصنف Animal ‏(Dog|Cat) الذي سينفذ التابع makeNoise إلى وقت التنفيذ وفقًا للخاصيّة ضمن الصنف User.

‎$this، ‏self، ‏static ونمط المفردة (singleton)

نستخدم ‎$this للإشارة إلى الكائن الحالي وself للإشارة إلى الصنف الحالي، وبمعنى آخر نستخدم ‎$this->member للعناصر غير الساكنة وself::$member‎ للعناصر الساكنة.

لاحظ الفرق في المثال التالي الذي تستخدم فيه الدالة sayHello()‎ ‏‎$this وتستخدم الدالة ‎sayGoodbye()‎ ‏self:

class Person {
    private $name;

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

    public function getName() {
        return $this->name;
    }

    public function getTitle() {
        return $this->getName()." the person";
    }

    public function sayHello() {
        echo "Hello, I'm ".$this->getTitle()."<br/>";
    }

    public function sayGoodbye() {
        echo "Goodbye from ".self::getTitle()."<br/>";
    }
}

class Geek extends Person {
    public function __construct($name) {
        parent::__construct($name);
    }

    public function getTitle() {
        return $this->getName()." the geek";
    }
}

$geekObj = new Geek("Ludwig");
$geekObj->sayHello();
$geekObj->sayGoodbye();

تشير الكلمة المفتاحية static إلى أي صنف في الهرمية تستدعي عليه التابع، وتسمح بإعادة استخدام أفضل لخصائص الصنف الساكنة عندما تورَّث الأصناف.

إليك المثال التالي:

class Car {
    protected static $brand = 'unknown';

    public static function brand() {
        return self::$brand."\n";
    }
}

class Mercedes extends Car {
    protected static $brand = 'Mercedes';
}

class BMW extends Car {
    protected static $brand = 'BMW';
}

echo (new Car)->brand();
echo (new BMW)->brand();
echo (new Mercedes)->brand();

نتيجة الشيفرة السابقة:

unknown
unknown
unknown

وذلك لأنّ self تشير إلى الصنف Car بغض النظر من أين اُستدعت brand()‎، لذا فأنت تحتاج لاستخدام static للإشارة إلى الصنف الصحيح وذلك بالشكل التالي:

class Car {
    protected static $brand = 'unknown';

    public static function brand() {
        return static::$brand."\n";
    }
}

class Mercedes extends Car {
    protected static $brand = 'Mercedes';
}

class BMW extends Car {
    protected static $brand = 'BMW';
}

echo (new Car)->brand();
echo (new BMW)->brand();
echo (new Mercedes)->brand();

ستنتج هذه الشيفرة الخرج المطلوب والذي هو:

unknown
BMW
Mercedes

نمط المفردة (singleton)

إذا كان لديك كائن مكلّف بإنشاء اتصال مع مورد خارجي أو يمثّل اتصال إلى مورد خارجي معين فأنت تحتاج إلى إعادة استخدام اتصال بقاعدة البيانات حيث لا يوجد تجمّع اتصال أو مقبس إلى نظام آخر، عندها يمكنك أن تستخدم الكلمات المفتاحية static وself في الصنف ليصبح بنمط المفردة. يوجد آراء مختلفة حول نمط المفردة فيما إذا كان يجب استخدامه أو لا ولكن يبقى له استخداماته.

class Singleton {
    private static $instance = null;

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

    private function __construct() {
        ….
    }
}

عرّفنا في الشيفرة السابقة خاصيّة ساكنة خاصة ‎$instance لتحمل مرجعية الكائن، وبما أنّها ساكنة فستُشارك بين كل كائنات هذا النوع.

يستخدم التابع getInstance()‎ طريقة التهيئة المُرجأة (lazy instantiation) لتأخير إنشاء الكائنات إلى آخر لحظة ممكنة لأنك لا تريد أن تمتلئ الذاكرة بكائنات غير مستخدمة، وأيضًا تحافظ على الوقت وعلى المعالج عند تحميل الصفحة إذ لا تُحمَّل كائنات غير ضرورية. يتحقق التابع فيما إذا ضُبط الكائن وأُنشئ أم لا وتعيده وهذا يضمن إنشاء كائن واحد فقط من هذا النوع.

كما عرّفنا الباني أنه خاص (private) حتى نضمن أنه من غير الممكن إنشاؤه من الخارج باستخدام الكلمة المفتاحية new. إذا كنت تحتاج للوراثة من هذا الصنف فقط غيّر الكلمات المفتاحية private إلى protected.

لاستخدام هذا الكائن تكتب الشيفرة التالية:

$singleton = Singleton::getInstance();

تعريف صنف أساسي

يتألف الكائن في PHP من متغيرات ودوال، وتنتمي الكائنات إلى صنف يعرّف المتغيرات والدوال التي تحتويها كل كائنات هذا الصنف، صياغة تعريف الصنف هي كالتالي:

class Shape {
    public $sides = 0;

    public function description() {
        return "A shape with $this->sides sides.";
    }
}

عندما يُعرَّف صنف يمكنك الاستنساخ منه بالشكل التالي:

$myShape = new Shape();

يمكن الوصول لمتغيرات ودوال الكائن بالطريقة التالية:

$myShape = new Shape();
$myShape->sides = 6;

print $myShape->description(); 
// A shape with 6 sides

الباني

يمكن أن تعرّف الأصناف تابع باني خاص ‎__construct()‎ يُنفَّذ كجزء من إنشاء الكائن، ويستخدم عادةً لإعطاء القيم الابتدائية لكائن:

class Shape {
    public $sides = 0;

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

    public function description() {
        return "A shape with $this->sides sides.";
    }
}

$myShape = new Shape(6);
print $myShape->description();
// A shape with 6 sides

توسيع صنف آخر

يمكن لتعريفات الصنف أن توسّع تعريفات صنف موجود وتضيف المزيد من المتغيرات والدوال وتعدّل في المتغيرات والدوال المعرفة في الصنف الأب، إليك صنف يوسّع الصنف في المثال السابق:

class Square extends Shape {
    public $sideLength = 0;

    public function __construct($sideLength) {
        parent::__construct(4);
        $this->sideLength = $sideLength;
    }

    public function perimeter() {
        return $this->sides * $this->sideLength;
    }

    public function area() {
        return $this->sideLength * $this->sideLength;
    }
}

يتضمن الصنف Square متغيرات ودوال لكل من الصنفين Square وShape.

$mySquare = new Square(10);
print $mySquare->description()/ // A shape with 4 sides
print $mySquare->perimeter() // 40
print $mySquare->area() // 100

الأصناف المجهولة

قُدِّمت الأصناف المجهولة في الإصدار PHP 7 لتمكين إنشاء نسخة واحدة بسيطة من الصنف، ويمكن لها أن تأخذ وسائط الباني وتوسّع أصناف أخرى وتنفّذ واجهات وتستخدم السمات (traits) مثل الأصناف العادية تمامًا، والنموذج الأساسي لها كالتالي:

new class("constructor argument") {
    public function __construct($param) {
        var_dump($param);
    }
}; // string(20) "constructor argument"

لا يوفر تداخل صنف مجهول ضمن صنف آخر إمكانية الوصول إلى التوابع أو الخاصيّات الخاصة أو المحمية للصنف الخارجي، يمكن الوصول إلى الخاصيات والتوابع المحمية للصنف الخارجي بتوسيعه من قِبل صنف مجهول ويمكن الوصول إلى الخاصيات والتوابع الخاصة للصنف الخارجي بتمريرها إلى باني الصنف المجهول. مثال:

class Outer {
    private $prop = 1;
    protected $prop2 = 2;

    protected function func1() {
        return 3;
    }

    public function func2() {

        // الخاصة $this->prop التمرير عبر الخاصيّة
        return new class($this->prop) extends Outer {
            private $prop3;
            public function __construct($prop) {
                $this->prop3 = $prop;
            }
            public function func3() {
                // (1)
                return $this->prop2 + $this->func1() + $this->prop3;
            }
        };
    }
}

echo (new Outer)->func2()->func3(); // 6

في الموضع (1) الوصول إلى الخاصيّة المحمية Outer::$prop2 والتابع المحمي Outer::func1()‎ والخاصية المحلية self::$prop3 الخاصة من Outer::$prop.

ترجمة -وبتصرف- للفصل [Classes and Objects] من كتاب PHP Notes for Professionals book

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...