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

الانعكاس (Reflection) وحقن الاعتمادية في PHP


سارة محمد2

الانعكاس

كشف ميزة الأصناف أو الكائنات

يمكن إجراء كشف ميزة الأصناف جزئيًا مع الدالتين property_exists وmethod_exists.

class MyClass {
    public $public_field;
    protected $protected_field;
    private $private_field;
    static $static_field;
    const CONSTANT = 0;
    public function public_function() {}
    protected function protected_function() {}
    private function private_function() {}
    static function static_function() {}
}

// التحقق من الخاصيات
$check = property_exists('MyClass', 'public_field'); // true
$check = property_exists('MyClass', 'protected_field'); // true

// PHP 5.3.0 بدءًا من true
$check = property_exists('MyClass', 'private_field'); 
$check = property_exists('MyClass', 'static_field'); // true
$check = property_exists('MyClass', 'other_field'); // false

// التحقق من التوابع
$check = method_exists('MyClass', 'public_function'); // true
$check = method_exists('MyClass', 'protected_function'); // true
$check = method_exists('MyClass', 'private_function'); // true
$check = method_exists('MyClass', 'static_function'); // true

$check = property_exists('MyClass', 'CONSTANT'); // false
$check = property_exists($object, 'CONSTANT'); // false

يمكننا باستخدام الصنف ReflectionClass كشف الثوابت أيضًا:

$r = new ReflectionClass('MyClass');
$check = $r->hasProperty('public_field'); // true
$check = $r->hasMethod('public_function'); // true
$check = $r->hasConstant('CONSTANT'); // true

تعمل الشيفرة السابقة أيضًا مع أعضاء الصنف الخاصة والمحمية و/أو الساكنة.

ملاحظة: يمكن استخدام الدالتين property_exists وmethod_exists مع كائن من الصنف بدلًا من الصنف أما لاستخدام الانعكاس مع الكائنات نستخدم الصنف ReflectionObject بدلًا من ReflectionClass.

اختبار التوابع الخاصة/المحمية

من المفيد أحيانًا اختبار التوابع الخاصة والمحمية والعامة أيضًا.

class Car
{
    protected function drive($argument)
    {
        return $argument;
    }

    private static function stop()
    {
        return true;
    }
}

أسهل طريقة لاختبار تابع قيادة (drive method) هي استخدام الانعكاس.

class DriveTest
{
    /**
    * @ اختبار
    */
    public function testDrive()
    {
        // التهيئة
        $argument = 1;
        $expected = $argument;
        $car = new \Car();
        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('drive');
        $method->setAccessible(true);

        // منطق الاستدعاء
        $result = $method->invokeArgs($car, [$argument]);

        // الاختبار
        $this->assertEquals($expected, $result);
    }
}

نمرر null مكان نسخة الصنف إذا كان التابع ساكنًا.

class StopTest
{
    /**
    * @test
    */
    public function testStop()
    {
        // التهيئة
        $expected = true;
        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('stop');
        $method->setAccessible(true);

        // منطق الاستدعاء
        $result = $method->invoke(null);
        // الاختبار

        $this->assertEquals($expected, $result);
    }
}

الوصول إلى متغيرات الأعضاء الخاصة والمحمية

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

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

class Car
{
    protected $color

    public function setColor($color)
    {
        $this->color = $color;
    }

    public function getColor($color)
    {
        return $this->color;
    }
}

ينشئ العديد من المطورين كائنًا من الصنف السابق لاختباره، يضبطون لون السيارة باستخدام Car::setColor()‎ ويستعيدونه باستخدام Car::getColor()‎ ويوازنون القيمة المُستعادة مع القيمة المضبوطة:

/**
* @اختبار
* @ \Car::setColor يغطي
*/
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);
    $getColor = $car->getColor();

    $this->assertEquals($color, $reflectionColor);
}

قد تبدو الأمور جيدة، يعيد كل Car::getColor()‎ قيمة المتغير المحمي Car::$color لكن هذا الاختبار خاطئ بطريقتين:

  • ينفذ Car::getColor()‎ الذي هو خارج نطاق هذا الاختبار.
  • يعتمد على Car::getColor()‎ الذي قد يكون فيه خطأ مما يجعل الاختبار إيجابيًا أو سلبيًا بشكلٍ خاطئ.

لنلقي نظرة لماذا يجب أن نستخدم الانعكاس بدلًا من Car::getColor()‎ في وحدة الاختبار، بفرض أُسندت مهمة للمطور لإضافة "Metallic" لكل لون سيارة لذا سيحاول تعديل Car::getColor()‎ لإضافة "Metallic" قبل لون السيارة:

class Car
{
    protected $color

    public function setColor($color)
    {
        $this->color = $color;
    }

    public function getColor($color)
    {
        return "Metallic "; $this->color;
    }
}

لاحظ أنّ المطور استخدم فاصلة منقوطة بدلًا من عامل الدمج لإضافة "Metallic" قبل لون السيارة، وفي النتيجة كلما استُدعي التابع Car::getColor()‎ ستكون القيمة المعادة "Metallic" بغض النظر عن لون السيارة الفعلي وستفشل نتيجة اختبار الوحدة حتى لو عمل التابع Car::setColor()‎ بشكلٍ صحيح ولم يتأثر بهذا التغيير.

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

لنطّلع على الشيفرة ثم نقسّمها:

/**
* @ اختبار
* @ \Car::setColor يغطي
*/
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);

    $reflectionOfCar = new \ReflectionObject($car);
    $protectedColor = $reflectionOfForm->getProperty('color');
    $protectedColor->setAccessible(true);
    $reflectionColor = $protectedColor->getValue($car);

    $this->assertEquals($color, $reflectionColor);
}

إليك الآن كيف نستخدم الانعكاس لنحصل على قيمة Car::$color في الشيفرة السابقة:

  • ننشئ كائن ReflectionObject يمثّل كائن السيارة لدينا.
  • نحصل على ReflectionProperty من أجل Car::$color (يمثل هذا المتغير Car::$color)
  • نجعل Car::$color قابلًا للوصول.
  • نحصل على القيمة من Car::$color.

كما لاحظت يمكننا الحصول على قيمة Car::$color باستخدام الانعكاس دون استدعاء Car::getColor()‎ أو أي دالة مساعدة أخرى مما قد يسبب نتائج اختبار غير صالحة، الأن أصبحت وحدة اختبار Car::setColor()‎ آمنة ودقيقة.

حقن الاعتمادية

حقن الاعتمادية (Dependency Injection) هو مصطلح زائف "لتمرير الأشياء"، وما يعنيه حقًا هو تمرير اعتماديات كائن عبر الباني و/أو التوابع الضابطة بدلًا من إنشائهم عند إنشاء كائن داخل كائن، وقد يشير حقن الاعتمادية إلى حاويات حقن الاعتمادية التي تشغّل البناء والحقن.

حقن الباني

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

في المثال التالي يعتمد الصنف Component على نسخة من الصنف Logger لكنه لا ينشئ واحدة، بدلًا من ذلك يتطلب نسخة لتمريرها كمعامل إلى الباني:

interface Logger {
    public function log(string $message);
}

class Component {
    private $logger;

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

تبدو الشيفرة مشابهًا لما يلي دون حقن الاعتمادية:

class Component {
    private $logger;

    public function __construct() {
        $this->logger = new FooLogger();
    }
}

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

في المثال السابق حيث نستخدم حقن الاعتمادية بدلًا من ذلك، يمكننا التغيير ببساطة إلى Logger مختلفة إذا اضطررنا لذلك، فمثلًا قد نستخدم تنفيذ Logger يسجل الدخول إلى موقع مختلف أو يستخدم تنسيق تسجيل دخول مختلف أو يسجل الدخول إلى قاعدة البيانات بدلًا من ملف.

حقن التابع الضابط

يمكن أن تحقن التوابع الضابطة أيضًا الاعتماديات.

interface Logger {
    public function log($message);
}

class Component {
    private $logger;
    private $databaseConnection;

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

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

    public function core() {
        $this->logSave();
        return $this->databaseConnection->save($this);
    }

    public function logSave() {
        if ($this->logger) {
            $this->logger->log('saving');
        }
    }
}

يعدّ هذا أمرًا مهمًا عندما لا تعتمد الوظيفة الأساسية للصنف على الاعتمادية للعمل.

الاعتمادية الوحيدة المطلوبة هنا هي DatabaseConnection لذا فهي في الباني، الاعتمادية Logger اختيارية لذا لا نحتاج لتكون جزءًا من الباني مما يجعل الصنف أسهل في الاستخدام.

لاحظ أنّه عند استخدام حقن التابع الضابط من الأفضل توسيع الوظيفة بدلًا من استبدالها، عند ضبط اعتمادية لا يوجد ما يؤكد أنّ الاعتمادية لن تتغير عند نقطة معينة مما قد يؤدي إلى نتائج غير متوقعة، فمثلًا يمكن ضبط FileLogger في البداية ثم ضبط MailLogger وهذا يكسر التغليف ويجعل من الصعب العثور على تسجيلات الدخول لأننا نستبدل التبعية.

يجب أن نضيف اعتمادية مع حقن التابع الضابط لمنع حدوث هذا كما في التالي:

interface Logger {
    public function log($message);
}

class Component {
    private $loggers = array();
    private $databaseConnection;

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

    public function addLogger(Logger $logger) {
        $this->loggers[] = $logger;
    }

    public function core() {
        $this->logSave();
        return $this->databaseConnection->save($this);
    }

    public function logSave() {
        foreach ($this->loggers as $logger) {
            $logger->log('saving');
        }
    }
}

في مثل هذا المثال كلما سنستخدم الوظيفة الأساسية فإنّها لن تنكسر حتى لو لم تُضاف اعتمادية مسجل وأي مسجل سيُضاف سيُستخدم مع أنّه يمكن إضافة مسجل آخر، نحن نوسع الوظيفة بدلًا من استبدالها.

حقن الحاوية

يمكن أن ننظر إلى حقن الاعتمادية في سياق استخدام حاوية حقن الاعتمادية (DIC) على أنّه مجموعة عليا من حقن الباني، تحلل حاوية حقن الاعتمادية تلميحات نوع باني الصنف وتعالج احتياجاته، نحتاج حقن الاعتماديات بشكلٍ فعال من أجل تنفيذ النسخ.

يتجاوز التنفيذ الدقيق نطاق هذا الملف ولكن في أصله تعتمد حاوية حقن الاعتمادية على استخدام بصمة الصنف.

namespace Documentation;

class Example
{
    private $meaning;

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

لنسخه بطريقة تلقائية يعتمد معظم الوقت على نظام التحميل التلقائي.

// القديمة PHP إصدارات
$container->make('Documentation\Example');

// PHP 5.5 بدءًا من الإصدار
$container->make(\Documentation\Example::class);

إذا كنت تستخدم إصدار PHP 5.5 على الأقل وتريد الحصول على اسم صنف كما في الطريقة في الشيفرة السابقة فإنّ الطريقة الصحيحة هي الثانية، بهذه الطريقة يمكنك إيجاد استخدامات الصنف بسرعة باستخدام بيئة تطوير متكاملة (IDE) حديثة مما يساعدك بشكلٍ كبير مع عملية إعادة التصميم (potential) المحتملة.

يعلم الصنف Documentation\Example في هذه الحالة أنّه يحتاج Meaning وستنشئ حاوية حقن الاعتمادية بدورها نسخة من النوع Meaning، لا يحتاج التنفيذ الفعلي إلى الاعتماد على النسخة المستهلَكة.

بدلًا من ذلك نضع قواعد في الحاوية قبل إنشاء الكائن توضح كيفية نسخ أنواع محددة عند الحاجة وهذا له عدة إيجابيات، كما أنّ حاوية حقن الاعتمادية تستطيع:

  • مشاركة النسخ المشتركة
  • توفير مصنع (factory) لحل بصمة النوع
  • حل بصمة واجهة

إذا عرّفنا قواعد تصف كيف يحتاج نوع محدد للإدارة يمكننا تحقيق تحكم دقيق على الأنواع المشتركة أو المنسوخة أو المُنشأة من مصنع.

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


×
×
  • أضف...