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

مفهوم السمات (Traits) في PHP


سارة محمد2

ما هي السمة (Traits)؟

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

trait Talk {
    /** @var string */
    public $phrase = 'Well Wilbur...';

    public function speak() {
        echo $this->phrase;
    }
}

class MrEd extends Horse {
    use Talk;
    public function __construct() {
        $this->speak();
    }

    public function setPhrase($phrase) {
        $this->phrase = $phrase;
    }
}

لدينا في الشيفرة السابقة الصنف MrEd يوسّع الصنف Horse، لكن ليس كل كائنات الصنف Horse لديها السمة Talk، لنرى ما تأثير ذلك.

نعرّف أولًا السمة (Traits) ويمكننا استخدامها مع التحميل التلقائي وفضاء الأسماء ثمّ نضمنها في الصنف MrEd باستخدام الكلمة المفتاحية use، ستلاحظ أنّ MrEd يستخدم دوال ومتغيرات Talk دون تعريفها أي أنّ هذه الدوال والمتغيرات كلها معرفة في الصنف MrEd الآن وكأننا عرفناها داخل الصنف (نسخنا ولصقنا المتغيرات والدوال).

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

متى يجب أن أستخدم السمة؟ عندما تريد استخدام السمة يجب أن تسأل نفسك السؤال المهم: هل يمكنني تجنب استخدام السمة بإعادة هيكلة شيفرتي؟ غالبًا سيكون الجواب نعم، السمات هي حالات هامشيّة تسببها الوراثة المفردة ويمكن أن يكون استخدامها بشكل مفرط مغريًا لكنها تقدم مصدرًا آخر لشيفرتك مما يعني أنّ هناك طبقة أخرى من التعقيد. تعاملنا في المثال السابق مع 3 أصناف فقط لكن السمات تعني أنّه يمكنك التعامل مع أكثر من ذلك بكثير، ومن أجل كل سمة يصبح التعامل مع صنفك أصعب بكثير إذ يجب عليك الرجوع إلى كل سمة لمعرفة ما الذي تعرّفه، كنتيجة يجب أن تحافظ على عدد سمات قليل قدر الإمكان.

السمات لتسهيل إعادة استخدام الشيفرة الأفقية

بفرض أنّه لدينا واجهة للتسجيل:

interface Logger {
    function log($message);
}

ولدينا صنفين ينفذان هذه الواجهة:

class FileLogger implements Logger {
    public function log($message) {
        // إضافة رسالة تسجيل إلى ملف ما
    }
}

class ConsoleLogger implements Logger {
    public function log($message) {
        // رسالة تسجيل إلى الطرفية
    }
}

إذا عرّفت الآن صنفًا آخر Foo والذي تريده أيضًا أن يؤدي مهام التسجيل باستخدام الشيفرة التالية:

class Foo implements Logger {
    private $logger;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function log($message) {
        if ($this->logger) {
            $this->logger->log($message);
        }
    }
}

ينفّذ الآن الصنف Foo الواجهة Logger لكنه يعتمد وظيفيًا على تنفيذ Logger الممرر إليه عبر الدالة setLogger()‎، إذا أردنا الآن أن يكون لدينا الصنف Bar وله نفس آلية التسجيل هذه يجب نسخ هذا الجزء من المنطق داخله، لكن نعرّف سمة بدلًا من تكرار الشيفرة:

trait LoggableTrait {
    protected $logger;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function log($message) {
        if ($this->logger) {
            $this->logger->log($message);
        }
    }
}

عرّفنا الآن المنطق في سمة ويمكننا استخدامها لإضافته إلى الأصناف Foo وBar:

class Foo {
    use LoggableTrait;
}

class Bar {
    use LoggableTrait;
}

ويمكننا استخدام الصنف Foo كما في الشيفرة التالية مثلًا:

$foo = new Foo();
$foo->setLogger( new FileLogger() );
$foo->log('my beautiful message');

لاحظ كيف استخدمنا السمة كوكيل (proxy) لاستدعاء تابع التسجيل على نسخة Foo.

حل التضارب

يمكن أن تؤدي محاولة استخدام عدة سمات في صف واحد إلى مشاكل تتعلق بالتوابع المتضاربة، عندها تحتاج لحل هذه التضاربات يدويًا، إليك المثال التالي:

trait MeowTrait {
    public function say() {
        print "Meow \n";
    }
}

trait WoofTrait {
    public function say() {
        print "Woof \n";
    }
}

abstract class UnMuteAnimals {
    abstract function say();
}

class Dog extends UnMuteAnimals {
    use WoofTrait;
}

class Cat extends UnMuteAnimals {
    use MeowTrait;
}

لنحاول الآن إنشاء الصنف التالي:

class TalkingParrot extends UnMuteAnimals {
    use MeowTrait, WoofTrait;
}

سيرجع مفسر PHP خطأً فادحًا:

Fatal error: Trait method say has not been applied, because there are collisions with other trait methods
on TalkingParrot

لحل مشكلة التضارب يمكننا القيام بما يلي:

  • نستخدم الكلمة المفتاحية insteadof لاستخدام تابع من سمة بدلًا من تابع من سمة أخرى.
  • ننشئ كنية للتابع مع الباني مثل WoofTrait::say as sayAsDog;‎
class TalkingParrotV2 extends UnMuteAnimals {
    use MeowTrait, WoofTrait {
        MeowTrait::say insteadof WoofTrait;
        WoofTrait::say as sayAsDog;
    }
}

$talkingParrot = new TalkingParrotV2();
$talkingParrot->say();
$talkingParrot->sayAsDog();

ستنتج هذه الشيفرة الخرج التالي:

Meow
Woof

تنفيذ نمط مفردة (Singleton) باستخدام السمات

ملاحظة: لا يؤيد هذا المثال استخدام نمط المفردة كثيرًا إنما يجب استخدامها بكثير من الحذر.

يوجد في PHP طريقة معيارية لتنفيذ المفردة:

public class Singleton {
    private $instance;

    private function __construct() { };

    public function getInstance() {
        if (!self::$instance) {
            // new Singleton() تكافئ الشيفرة new self()
            self::$instance = new self();
        }
        return self::$instance;
    }

    // منع نسخ الكائن
    protected function __clone() { }

    // منع سَلسَلة الكائن
    protected function __sleep() { }

    // منع عدم سَلسَلة الكائن
    protected function __wakeup() { }
}

من الجيد استخلاص هذا السلوك ضمن سمة لمنع تكرار الشيفرة:

trait SingletonTrait {
    private $instance;

    protected function __construct() { };

    public function getInstance() {
        if (!self::$instance) {
            // إلى الصنف الذي يستخدم السمة new self() يشير
            self::$instance = new self();
        }
        return self::$instance;
    }

    protected function __clone() { }

    protected function __sleep() { }

    protected function __wakeup() { }
}

يمكن الآن لأي صنف يريد أن يعمل كمفردة أن يستخدم السمة:

class MyClass {
    use SingletonTrait;
}

// خطأ! مرئية الباني ليست عامة
$myClass = new MyClass();
$myClass = MyClass::getInstance();

// كل الاستدعاءات التالية ستفشل بسبب مرئية التابع
$myClassCopy = clone $myClass; 
$serializedMyClass = serialize($myClass); 
$myClass = deserialize($serializedMyclass);

على الرغم من أنّه من المستحيل الآن سَلسَلة المفردة إلا أنّه لا يزال من المفيد حجب تابع عدم السَلسلة.

السمات للمحافظة على الأصناف نظيفة

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

لنفرض مثلًا أنّه لدينا واجهتين وصف ينفّذ هاتين الواجهتين:

interface Printable {
    public function print();
    // ...توابع الواجهة الأخرى
}

interface Cacheable {
    // توابع الواجهة
}

class Article implements Cachable, Printable {
    // يجب أن ننفذ هنا كل توابع الواجهة
    public function print(){ {
        /* شيفرة لطباعة المقالة */
    }
}

يمكن استخدام سمات مستقلة لتنفيذ هذه الواجهة بدلًا من تنفيذ كل توابع الواجهة في الصنف Article للحفاظ على الصنف أصغر وفصل شيفرة تنفيذ الواجهة من الصنف.

مثلًا لتنفيذ الواجهة Printable يمكننا إنشاء هذه السمة:

trait PrintableArticle {
    // هنا تنفيذ توابع الواجهة
    public function print() {
        /* شيفرة لطباعة المقالة */
    }
}

ثم نجعل الصنف يستخدم السمة:

class Article implements Cachable, Printable {
    use PrintableArticle;
    use CacheableArticle;
}

تتمثل الفوائد الأساسية في أنّ توابع تنفيذ الواجهة ستنفصل عن بقية الصنف وتُخزَّن في سمة وهذه السمة تتحمل وحدها مسؤولية تنفيذ الواجهة من أجل هذا النوع المعين من الكائنات.

استخدام عدة سمات

trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait World {
    public function sayWorld() {
        echo 'World';
    }
}

class MyHelloWorld {
    use Hello, World;
    public function sayExclamationMark() {
        echo '!';
    }
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();

خرج المثال السابق:

Hello World!

تغيير مرئية التابع

trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

// sayHello تغيير مرئية
class MyClass1 {
    use HelloWorld { sayHello as protected; }
}

// كنية تابع مع تغيير مرئية
// لم تتغير sayHello مرئية
class MyClass2 {
    use HelloWorld { sayHello as private myPrivateHello; }
}

تنفيذ هذا المثال:

(new MyClass1())->sayHello();
// Fatal error: Uncaught Error: Call to protected method MyClass1::sayHello()

(new MyClass2())->myPrivateHello();
// Fatal error: Uncaught Error: Call to private method MyClass2::myPrivateHello()

(new MyClass2())->sayHello();
// Hello World!

انتبه أنّه في المثال الأخير في MyClass2 التابع الأصلي غير المُكنّى من trait HelloWorld يبقى كما هو قابلًا للوصول.

ترجمة -وبتصرف- للفصل [Traits] من كتاب 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.


×
×
  • أضف...