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

شرح المولدات وتوضيح مفهوم المغلف (closure) في PHP


سارة محمد2

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

تشبه التعليمة yield تعليمة return باستثناء أنّه بدلًا من إيقاف تنفيذ الدالة وإرجاع شيء ما، فإنّها تُرجع كائن Generator وتوقف تنفيذ دالة المولِّد، إليك مثال عن دالة مجال مكتوبة كمولِّد:

function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // ‫لاحظ أنّ المتغير ‎$i مسبوق بالكلمة المفتاحية yield.
        yield $i;
    }
}

يمكنك ملاحظة أنّ هذه الدالة ترجع كائنًا من النوع مولِّد Generator من خلال فحص الخرج باستخدام الدالة var_dump:

var_dump(gen_one_to_three())

/*
class Generator (0) {
}
*/

القيم المعادة (Yielding)

يمكن تكرار الكائن Generator مثل المصفوفة:

foreach (gen_one_to_three() as $value) {
    echo "$value\n";
}

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

1
2
3

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

بالإضافة إلى إعادة قيم يمكنك إعادة أزواج قيمة/مفتاح:

function gen_one_to_three() {
    $keys = ["first", "second", "third"];
    for ($i = 1; $i <= 3; $i++) {
        // ‫لاحظ أنّ المتغير ‎$i‎ هو قيمة معادة
        yield $keys[$i - 1] => $i;
    }
}

foreach (gen_one_to_three() as $key => $value) {
    echo "$key: $value\n";
}

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

first: 1
second: 2
third: 3

قراءة ملف كبير باستخدام مولد

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

<?php
    class CsvReader
    {
        protected $file;
        public function __construct($filePath) {
            $this->file = fopen($filePath, 'r');
        }

        public function rows()
        {
            while (!feof($this->file)) {
            $row = fgetcsv($this->file, 4096);
            yield $row;
        }

        return;
    }
}

$csv = new CsvReader('/path/to/huge/csv/file.csv');

foreach ($csv->rows() as $row) {
    // ‫القيام بشيء ما مع صف CSV
}

لماذا نستخدم المولِّد؟

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

بفرض لدينا الدالة التالية:

function randomNumbers(int $length)
{
    $array = [];
    for ($i = 0; $i < $length; $i++) {
        $array[] = mt_rand(1, 10);
    }

    return $array;
}

تولِّد الدالة السابقة مصفوفة تُملأ بأرقام عشوائية، قد نستخدمها بالشكل randomNumbers(10)‎ مثلًا مما يعطينا مصفوفة من 10 أرقام عشوائية، لكن ماذا لو أردنا توليد مليون رقمًا عشوائيًا؟ يمكننا القيام بذلك بكتابة randomNumbers(1000000)‎ لكن هذا يكلّف ذاكرة، إذ أنّ مليون عدد صحيح مخزّن في مصفوفة يحتاج إلى حوالي 33 ميجابايت من الذاكرة.

$startMemory = memory_get_usage();
$randomNumbers = randomNumbers(1000000);
echo memory_get_usage() - $startMemory, ' bytes';

هذا بسبب توليد مليون رقم عشوائي وإرجاعه مرةً واحدةً، بدلًا من توليد وإرجاع رقم في كل مرة، تعدّ المولِّدات طريقةً سهلةً لحل هذه المشكلة.

استخدام دالة send()‎ لتمرير القيم إلى مولد

تُشفَّر المولِّدات بسرعة وتكون في الكثير من الحالات بديلًا خفيفًا عن تنفيذات المكرِّر الثقيلة، ومع التنفيذ السريع يصبح لدينا نقصًا قليلًا في التحكم عندما يجب أن يتوقف المولِّد عن التوليد أو إذا كان يجب أن يولِّد شيئًا آخر. لكن يمكن تحقيق ذلك باستخدام دالة send()‎ مما يتيح لدالة الطلب إرسال المعاملات إلى المولِّد بعد كل حلقة.

بفرض أنّه نريد الوصول إلى كمية بيانات كبيرة باستخدام المولِّد في الشيفرة التالية:

function generateDataFromServerDemo()
{
    // في هذا المثال نُرسل التغذية الراجعة في كل تكرار للحلقة بدلًا من إرسال بيانات من الخادم
    $indexCurrentRun = 0;

    $timeout = false;
    while (!$timeout)
    {
        $timeout = yield $indexCurrentRun; //(1)
        $indexCurrentRun++;
    }
    yield 'X of bytes are missing. </br>';
}

// بدء استخدام المولِّد
$generatorDataFromServer = generateDataFromServerDemo ();
foreach($generatorDataFromServer as $numberOfRuns)
{
    if ($numberOfRuns < 10)
    {
        echo $numberOfRuns . "</br>";
    }
    else
    {
        // إرسال البيانات إلى المولِّد
        $generatorDataFromServer->send(true);

        // الوصول إلى العنصر الأخير (التلميح إلى عدد البايتات المفقودة)
        echo $generatorDataFromServer->current();
    }
}

في الموضع (1) تُمرَّر القيم إلى المستدعي، وفي المرة التالية يُستدعى المولِّد الذي يبدأ عند هذه التعليمة، إذا استخدمنا الدالة send()‎ فإنّ المتغير ‎$timeout سيأخذ هذه القيمة.

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

0
1
2
3
4
5
6
7
8
9
X bytes are missing

المغلف (closure)

الاستخدام الأساسي للمغلِّف

يكافئ المغلِّف الدالة المجهولة (anonymous function) أي الدالة التي ليس لها اسم، حتى لو كان ذلك غير صحيح تقنيًا فإنّ سلوك المغلِّف نفس سلوك الدالة مع ميزات إضافية قليلة. مثال:

<?php
$myClosure = function() {
    echo 'Hello world!';
};

$myClosure();
// "Hello world!"

تذكر أنّ ‎$myClosure هو نسخة من مغلِّف (closure) لذا يجب أن تنتبه لما يمكنك القيام به (اطلع على الرابط).

الحالة التقليدية التي تحتاج فيها استخدام المغلِّف هي عندما تريد إعطاء شيء قابل للاستدعاء إلى دالة ما، مثل الدالة usort، في المثال التالي تُرتَّب المصفوفة حسب عدد الأبناء لكل أب:

<?php
$data = [
    [
        'name' => 'John',
        'nbrOfSiblings' => 2,
    ],
    [
        'name' => 'Stan',
        'nbrOfSiblings' => 1,
    ],
    [
        'name' => 'Tom',
        'nbrOfSiblings' => 3,
    ]
];
usort($data, function($e1, $e2) {
    if ($e1['nbrOfSiblings'] == $e2['nbrOfSiblings']) {
        return 0;
    }
    return $e1['nbrOfSiblings'] < $e2['nbrOfSiblings'] ? -1 : 1;
});

var_dump($data); 

// ‫سيظهر في الخرج Stan ثم John ثم Tom

استخدام متغيرات خارجية

من الممكن استخدام متغير خارجي داخل المُغلِّف باستخدام الكلمة المفتاحية الخاصة use، مثال:

<?php
$quantity = 1;

$calculator = function($number) use($quantity) {
    return $number + $quantity;
};

var_dump($calculator(2)); 
// "3"

ويمكنك أيضًا إنشاء دوال مغلِّفة ديناميكية، فمثلًا يمكنك إنشاء دالة تُرجع عملية حسابية معينة بالاعتماد على كمية تريد إضافتها:

<?php
function createCalculator($quantity) {
    return function($number) use($quantity) {
        return $number + $quantity;
    };
}

$calculator1 = createCalculator(1);
$calculator2 = createCalculator(2);

var_dump($calculator1(2)); 
// "3"
var_dump($calculator2(2)); 
// "4"

الربط الأساسي لمغلِّف

كما لاحظت سابقًا فإنّ المغلِّف هو نسخة من الصنف Closure ويمكن استدعاء توابع مختلفة عليه، مثل التابع bindTo الذي يُعطى مغلِّف ويرجع مغلِّفًا جديدًا مرتبطًا بكائن مُعطى، مثال:

<?php
$myClosure = function() {
    echo $this->property;
};

class MyClass
{
    public $property;
    public function __construct($propertyValue)
    {
        $this->property = $propertyValue;
    }
}

$myInstance = new MyClass('Hello world!');
$myBoundClosure = $myClosure->bindTo($myInstance);
$myBoundClosure(); 

// "Hello world!"

ربط المغلف والنطاق

بفرض لدينا المثال التالي:

<?php
$myClosure = function() {
    echo $this->property;
};
class MyClass
{
    public $property;
    public function __construct($propertyValue)
    {
        $this->property = $propertyValue;
    }
}

$myInstance = new MyClass('Hello world!');
$myBoundClosure = $myClosure->bindTo($myInstance);
$myBoundClosure(); 
// "Hello world!"

إذا حاولت تغيير مرئية الخاصيّة إلى محميّة (protected) أو خاصة (private)، ستحصل على خطأ فادح يشير إلى أنّه لا يمكنك الوصول إلى هذه الخاصيّة، وحتى لو كان المغلِّف مرتبطًا بالكائن فإنّ النطاق الذي يُنفَّذ فيه المغلِّف ليس بحاجة ليكون لديه إمكانية الوصول ولهذا السبب نستخدم المعامل الثاني للتابع bindTo.

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

<?php
$myClosure = function() {
    echo $this->property;
};

class MyClass
{
    private $property; 
    public function __construct($propertyValue)
    {
        $this->property = $propertyValue;
    }
}

$myInstance = new MyClass('Hello world!');
$myBoundClosure = $myClosure->bindTo($myInstance, MyClass::class);
$myBoundClosure(); 
// "Hello world!"

إذا لم يُستخدم المعامل الثاني فإنّ المُغلِّف يُنفَّذ في نفس السياق الذي أُنشئ فيه، فمثلًا إذا أُنشئ المغلِّف داخل تابع صنف والذي يُنفَّذ في سياق كائن سيكون له نفس نطاق هذا التابع:

<?php
class MyClass
{
    private $property;
    public function __construct($propertyValue)
    {
        $this->property = $propertyValue;
    }

    public function getDisplayer()
    {
        return function() {
            echo $this->property;
        };
    }
}

$myInstance = new MyClass('Hello world!');
$displayer = $myInstance->getDisplayer();
$displayer(); 
// "Hello world!"

ربط مغلِّف لاستدعاء واحد

بدءًا من الإصدار PHP7 أصبح من الممكن ربط مغلِّف باستدعاء واحد فقط بفضل التابع call، مثال:

<?php
class MyClass
{
    private $property;
    public function __construct($propertyValue)
    {
        $this->property = $propertyValue;
    }
}

$myClosure = function() {
    echo $this->property;
};

$myInstance = new MyClass('Hello world!');
$myClosure->call($myInstance); 
// "Hello world!"

لا داعي للقلق بشأن النطاق على عكس التابع bindTo، إذ أنّ النطاق المستخدم لهذا الاستدعاء هو نفس المستخدم عند الوصول إلى أو تنفيذ خاصية ‎$myInstance.

استخدام المغلِّفات لتنفيذ نمط المراقِب

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

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

<?php
class ObservedStuff implements SplSubject
{
    protected $property;
    protected $observers = [];

    public function attach(SplObserver $observer)
    {
        $this->observers[] = $observer;
        return $this;
    }

    public function detach(SplObserver $observer)
    {
        if (false !== $key = array_search($observer, $this->observers, true)) {
            unset($this->observers[$key]);
        }
    }

    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function getProperty()
    {
        return $this->property;
    }

    public function setProperty($property)
    {
        $this->property = $property;
        $this->notify();
    }
}

ثم نصرّح عن الصنف الذي سيمثّل المراقبين المختلفين:

<?php
class NamedObserver implements SplObserver
{
    protected $name;
    protected $closure;

    public function __construct(Closure $closure, $name)
    {
        $this->closure = $closure->bindTo($this, $this);
        $this->name = $name;
    }

    public function update(SplSubject $subject)
    {
        $closure = $this->closure;
        $closure($subject);
    }
}

لنبدأ بالاختبار الآن:

<?php
$o = new ObservedStuff;
$observer1 = function(SplSubject $subject) {
    echo $this->name, ' has been notified! New property value: ',         $subject->getProperty(), "\n";
};

$observer2 = function(SplSubject $subject) {
    echo $this->name, ' has been notified! New property value: ', $subject->getProperty(), "\n";
};

$o->attach(new NamedObserver($observer1, 'Observer1'))
->attach(new NamedObserver($observer2, 'Observer2'));
$o->setProperty('Hello world!');

/*
Observer1 has been notified! New property value: Hello world!
Observer2 has been notified! New property value: Hello world!
*/

لاحظ أنّ هذا المثال يعمل لأنّ المراقبين يتشاركون نفس الطبيعة (كلاهما "named observers").

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


×
×
  • أضف...