الجزء 8 - WordPress والبرمجة الموجهة للكائنات: مثال على WordPress - التنفيذ: الخيارات

نشرت: 2022-02-04

حتى الآن ، نحتاج فقط إلى تخزين الخيارات التي يحددها المستخدم ، لذلك استخدمنا إعدادات API. ومع ذلك ، يجب أن يكون المكون الإضافي الخاص بنا قادرًا على قراءة / كتابة الخيارات نفسها "لتذكر" عدد المرات التي حاول فيها عنوان IP تسجيل الدخول دون جدوى ، إذا كان مغلقًا حاليًا ، وما إلى ذلك.

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

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

دعنا نحول هذا إلى واجهة:

 interface Options { /** * Return the option value based on the given option name. * * @param string $name Option name. * @return mixed */ public function get( $name ); /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section ID. * @return bool Whether the option was added. */ public function set( $name, $value, $section_id ); /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section ID. */ public function remove( $name, $section_id ); }

من الناحية المثالية ، سنكون قادرين على التفاعل مع WordPress Options API ، من خلال القيام بشيء مثل هذا:

 $options = new WP_Options(); $options->get( 'retries' );

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

ستقوم فئة WP_Options الخاصة بنا بتنفيذ واجهة Options الخاصة بنا. بهذه الطريقة ، سنكون مستعدين إذا تغيرت احتياجاتنا في المستقبل. على سبيل المثال ، قد نحتاج إلى تخزين خياراتنا في جدول مخصص ، في قاعدة بيانات خارجية ، في الذاكرة (مثل Redis) ، سمها ما شئت. من خلال الاعتماد على التجريد (أي الواجهة) ، فإن تغيير شيء ما في التطبيق بسيط مثل إنشاء فئة جديدة تنفذ نفس الواجهة.

WP_Options

لنبدأ في كتابة فئة WP_Options بنا ، عن طريق استرداد جميع الخيارات باستخدام get_option() WordPress في مُنشئها.

 class WP_Options { /** * @var array Stored options. */ private $options; /** * WP_Options constructor. */ public function __construct() { $this->options = get_option( Plugin::PREFIX ); } }

نظرًا لأنه سيتم استخدام خاصية $options داخليًا ، فسنعلن أنها private ، لذا لا يمكن الوصول إليها إلا بواسطة الفئة التي حددتها ، فئة WP_Options .

الآن ، دعنا ننفذ واجهة Options الخاصة بنا باستخدام مشغل implements .

 class WP_Options implements Options { // ...

يصرخ IDE فينا إما للإعلان عن ملخص الفصل الخاص بنا أو تنفيذ أساليب get() و set() remove() المحددة في الواجهة.

لذا ، لنبدأ في تنفيذ هذه الأساليب!

الحصول على خيار

سنبدأ بطريقة get() ، والتي ستبحث عن اسم الخيار المحدد في خاصية $options الخاصة بنا ، وإما إرجاع قيمتها أو false إذا لم تكن موجودة.

 class WP_Options implements Options { private $options; public function __construct() { $this->options = get_option( Plugin::PREFIX ); } /** * Return the option value based on the given option name. * * @return mixed */ public function get( $option_name ) { if ( ! isset( $this->options[ $option_name ] ) ) { return false; } return $this->options[ $option_name ]; } }

حان الوقت الآن للتفكير في الخيارات الافتراضية.

الخيارات الافتراضية

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

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

ملاحظة: يتم كتابة اسم الثابت بأحرف كبيرة حسب الاصطلاح.

 const DEFAULT_OPTIONS = array( 'general_options' => array( 'allowed_retries' => 4, 'normal_lockout_time' => 1200, // 20 minutes 'max_lockouts' => 4, 'long_lockout_time' => 86400, // 24 hours 'hours_until_retries_reset' => 43200, // 12 hours 'site_connection' => 'direct', 'handle_cookie_login' => 'yes', 'notify_on_lockout_log_ip' => true, 'notify_on_lockout_email_to_admin' => false, 'notify_after_lockouts' => 4 ), 'state' => array( 'lockouts' => array(), 'retries' => array(), 'lockout_logs' => array(), 'total_lockouts' => 0 ) );

في المصفوفة المتداخلة DEFAULT_OPTIONS ، قمنا بتعيين قيمة افتراضية لجميع خياراتنا.

ما نرغب في القيام به بعد ذلك ، هو تخزين قيم الخيار الافتراضية في قاعدة البيانات بمجرد تهيئة المكون الإضافي ، باستخدام add_option() WordPress.

 class WP_Options { public function __construct() { $all_options = array(); foreach ( self::DEFAULT_OPTIONS as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; } $all_options = array_merge( $all_options, $section_options ); } $this->options = $all_options; } }

دعونا نلقي نظرة فاحصة على هذا المقتطف. أولاً ، نقوم بتكرار مصفوفة الخيارات الافتراضية واسترداد الخيارات باستخدام get_option() WordPress.

 foreach ( self::default_options as $section_id => $section_default_options ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $section_options = get_option( $db_option_name ); // ...

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

 if ( $section_options === false ) { add_option( $db_option_name, $section_default_options ); $section_options = $section_default_options; }

أخيرًا ، نقوم بتجميع خيارات جميع الأقسام.

 $all_options = array_merge( $all_options, $section_options );

وقم بتخزينها في خاصية $options حتى نتمكن من الوصول إليها لاحقًا.

 $this->options = $all_options;

سيحتوي جدول خيارات WordPress في قاعدة البيانات على صفين ، حيث يتكون option_name من بادئة المكون الإضافي المتسلسلة مع اسم القسم.

دعنا ننتقل الآن إلى بقية الأساليب التي نحتاج إلى تنفيذها.

تخزين خيار

وبالمثل ، نود تخزين خيار جديد بسهولة في قاعدة البيانات ، والكتابة فوق أي قيمة سابقة ، مثل هذا:

 $options = new Options(); $options->set( 'retries', 4 );

لذلك ، دعنا ننفذ طريقة set() ، والتي ستستخدم update_option() WordPress.

 /** * Store the given value to an option with the given name. * * @param string $name Option name. * @param mixed $value Option value. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was added. */ public function set( $name, $value, $section_ ) { $db_option_name = Plugin::PREFIX . '_' . $section_id; $stored_option = get_option( $db_option_name ); $stored_option[ $name ] = $value; return update_option( $db_option_name, $stored_option ); }

إزالة خيار

أخيرًا ، سننفذ طريقة remove() ، والتي ستعمل على ضبط الخيار على قيمته الأولية:

 /** * Remove the option with the given name. * * @param string $name Option name. * @param string $section_id Section id. Defaults to 'state'. * @return bool Whether the option was removed. */ public function remove( $name, $section_ ) { $initial_value = array(); if ( isset( self::DEFAULT_OPTIONS[ $section_id ][ $name ] ) ) { $initial_value = self::DEFAULT_OPTIONS[ $section_id ][ $name ]; } return $this->set( $name, $initial_value, $section_id ); }

لقد جمعنا كل شيء معًا في فصل دراسي واحد. يتم تغليف جميع البيانات المتعلقة بالخيارات (أي خصائصنا) وتفاصيل التنفيذ (أي الطرق التي نفذناها للتو) في فئة WP_Options .

التغليف / التجريد

تغليف كل شيء في صف واحد ، وإحاطة الأجزاء الداخلية (كما لو كان في كبسولة) ، "إخفاء" كل شيء عن العالم الخارجي ، هو ما نسميه التغليف . التغليف هو مفهوم أساسي آخر للبرمجة الشيئية.

استضافة موقع الويب الخاص بك مع Pressidium

ضمان استرداد الأموال لمدة 60 يومًا

اطلع على خططنا

باستخدام واجهة Options ، ركزنا على ما نفعله بخياراتنا بدلاً من كيفية القيام بذلك ، واستخلاص فكرة الخيارات ، وتبسيط الأشياء من الناحية المفاهيمية. هذا ما نسميه التجريد ، مفهوم أساسي آخر للبرمجة الشيئية.

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

التبعيات

لنفكر في السيناريو التالي:

هناك فئة Lockouts ، مسؤولة عن تحديد ما إذا كان يجب إغلاق عنوان IP ، وما يجب أن تكون مدة هذا الإغلاق ، إذا كان القفل النشط لا يزال ساريًا أو منتهي الصلاحية وما إلى ذلك. تحتوي هذه الفئة على طريقة should_get_locked_out() ، المسؤولة عن تحديد ما إذا كان يجب تأمين عنوان IP. ستحتاج هذه الطريقة إلى قراءة الحد الأقصى لعدد المحاولات المسموح بها قبل أن يتم تأمين عنوان IP ، وهي قيمة قابلة للتكوين ، مما يعني أنه يتم تخزينها كخيار .

لذا ، فإن الكود الذي وصفناه للتو سيبدو مشابهًا لهذا:

 class Lockouts { // ... /** * @var WP_Options An instance of `WP_Options`. */ private $options; /** * Lockouts constructor */ public function __construct() { $this->options = new WP_Options(); } /** * Return the number of retries. * * @return int */ private function get_number_of_retries() { // ... } /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; } // ... }

في الأساس ، نقوم بإنشاء مثيل جديد من WP_Options في المُنشئ ، ثم نستخدم هذا المثال لاسترداد قيمة الخيار allowed_retries .

هذا جيد تمامًا ، لكن علينا أن نضع في اعتبارنا أن فئة Lockouts لدينا تعتمد الآن على WP_Options . نسمي WP_Options تبعية .

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

يمكننا القضاء على هذه المشكلة عن طريق إعادة كتابة التعليمات البرمجية الخاصة بنا لاتباع مبدأ انعكاس التبعية .

فصل

ينص مبدأ انعكاس التبعية (DIP) ، "D" في SOLID ، على ما يلي:

  • يجب ألا تستورد الوحدات عالية المستوى أي شيء من الوحدات ذات المستوى المنخفض. كلاهما يجب أن يعتمد على الأفكار المجردة.
  • يجب ألا تعتمد التجريدات على التفاصيل. يجب أن تعتمد التفاصيل (عمليات التنفيذ الملموسة) على الأفكار المجردة.

في حالتنا ، فئة Lockouts هي "وحدة المستوى العالي" وتعتمد على "وحدة المستوى المنخفض" ، فئة WP_Options .

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

 class Lockouts { // ... /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; } // ... }

لذلك ، نقوم بحقن التبعية:

 $options = new WP_Options(); $lockouts = new Lockouts( $options );

لقد جعلنا للتو فصل Lockouts أسهل في الصيانة نظرًا لأنه يقترن الآن بشكل غير محكم WP_Options . بالإضافة إلى ذلك ، سنكون قادرين على السخرية من التبعيات ، مما يجعل اختبار الكود الخاص بنا أسهل. سيؤدي استبدال WP_Options بكائن يحاكي سلوكه إلى السماح لنا باختبار الكود الخاص بنا دون تنفيذ أي استفسارات في قاعدة البيانات.

 /** * Lockouts constructor. * * @param WP_Options $options */ public function __construct( WP_Options $options ) { $this->options = $options; }

على الرغم من أننا منحنا التحكم في تبعيات Lockouts إلى فئة أخرى (على عكس Lockouts التي تتحكم في التبعيات نفسها) ، لا تزال Lockouts تتوقع كائن WP_Options . بمعنى أنه لا يزال يعتمد على فئة WP_Options الملموسة ، بدلاً من التجريد. كما ذكرنا سابقًا ، يجب أن تعتمد كلتا الوحدتين على التجريدات .

دعونا نصلح ذلك!

 /** * Lockouts constructor. * * @param Options $options */ public function __construct( Options $options ) { $this->options = $options; }

وببساطة عن طريق تغيير نوع وسيطة $options من فئة WP_Options إلى واجهة Options ، تعتمد فئة Lockouts الخاصة بنا على التجريد ونحن أحرار في تمرير كائن DB_Options ، أو مثيل لأي فئة تنفذ نفس الواجهة ، لمنشئها.

مسؤولية واحدة

تجدر الإشارة إلى أننا استخدمنا طريقة تسمى should_get_locked_out() للتحقق مما إذا كان يجب إغلاق عنوان IP أم لا.

 /** * Check whether this IP address should get locked out. * * @return bool */ public function should_get_locked_out() { $retries = $this->get_number_of_retries(); $allowed_retries = $this->options->get( 'allowed_retries' ); return $retries % $allowed_retries === 0; }

يمكننا بسهولة كتابة سطر واحد مثل هذا:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) {

ومع ذلك ، فإن نقل هذا الجزء من المنطق إلى طريقته الصغيرة الخاصة به الكثير من الفوائد.

  • إذا تغير شرط تحديد ما إذا كان يجب إغلاق عنوان IP على الإطلاق ، فسنضطر فقط إلى تعديل هذه الطريقة (بدلاً من البحث عن جميع تكرارات عبارة if الخاصة بنا)
  • تصبح كتابة اختبارات الوحدة أسهل عندما تكون كل "وحدة" أصغر
  • يحسن قابلية قراءة الكود الخاص بنا كثيرًا

اقرأ هذا:

 if ( $this->should_get_locked_out() ) { // ...

يبدو لنا أسهل بكثير من قراءة ما يلي:

 if ( $this->get_number_of_retries() % $this->options->get( 'allowed_retries' ) === 0 ) { // ...

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

ينص مبدأ المسؤولية الفردية (SRP) ، "S" في SOLID ، على ما يلي:

"يجب أن تتحمل كل وحدة نمطية أو فئة أو وظيفة في برنامج الكمبيوتر المسؤولية عن جزء واحد من وظائف هذا البرنامج ، ويجب أن تغلف هذا الجزء."

أو كما يقول روبرت سي مارتن ("العم بوب"):

"يجب أن يكون للفصل سبب واحد فقط للتغيير".

إعادة النظر في ملف البرنامج المساعد الرئيسي

في الوقت الحالي ، يحتوي ملف المكون الإضافي الرئيسي الخاص بنا على هذا فقط:

 /** * Plugin Name: PRSDM Limit Login Attempts * Plugin URI: https://pressidium.com * Description: Limit rate of login attempts, including by way of cookies, for each IP. * Author: Pressidium * Author URI: https://pressidium.com * Text Domain: prsdm-limit-login-attempts * License: GPL-2.0+ * Version: 1.0.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; }

مرة أخرى ، سنلتف كل شيء في فئة البرنامج المساعد ، هذه المرة فقط لتجنب اصطدام التسمية.

 namespace Pressidium\Limit_Login_Attempts; if ( ! defined( 'ABSPATH' ) ) { exit; } class Plugin { /** * Plugin constructor. */ public function __construct() { // ... } }

سننشئ فئة Plugin هذه في نهاية الملف ، والتي ستقوم بتنفيذ الكود في مُنشئها.

 new Plugin();

في المُنشئ ، سنربط الإجراء plugins_loaded ، والذي يتم تشغيله بمجرد تحميل المكونات الإضافية النشطة.

 public function __construct() { add_action( 'plugins_loaded', array( $this, 'init' ) ); } public function init() { // Initialization }

سنقوم أيضًا باستدعاء require_files() لتحميل جميع ملفات PHP الخاصة بنا.

 public function __construct() { $this->require_files(); add_action( 'plugins_loaded', array( $this, 'init' ) ); } private function require_files() { require_once __DIR__ . '/includes/Sections/Section.php'; require_once __DIR__ . '/includes/Pages/Admin_Page.php'; require_once __DIR__ . '/includes/Pages/Settings_Page.php'; // ... }

أخيرًا ، سنهيئ المكون الإضافي الخاص بنا عن طريق إنشاء بعض الكائنات في طريقة init() .

ملاحظة: المقتطف التالي يحتوي فقط على جزء صغير من ملف البرنامج المساعد الرئيسي. يمكنك قراءة الملف الفعلي في مستودع GitHub للمكوِّن الإضافي.

 public function init() { $options = new Options(); $hooks_manager = new Hooks_Manager(); $settings_page = new Settings_Page( $options ); $hooks_manager->register( $settings_page ); // ... }

تنظيم الملفات

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

لقد حددنا بالفعل مساحة اسم ( Pressidium\Limit_Login_Attempts ) تحتوي على العديد من مساحات الأسماء الفرعية Pages Sections Fields Elements وما إلى ذلك.

 . ├── includes │ ├── Hooks │ │ ├── Actions.php │ │ ├── Filters.php │ │ └── Hooks_Manager.php │ ├── Pages │ │ ├── Admin_Page.php │ │ └── Settings_Page.php │ ├── Sections │ │ ├── Fields │ │ │ ├── Elements │ │ │ │ ├── Checkbox_Element.php │ │ │ │ ├── Custom_Element.php │ │ │ │ ├── Element.php │ │ │ │ ├── Number_Element.php │ │ │ │ └── Radio_Element.php │ │ │ └── Field.php │ │ └── Section.php │ └── WP_Options.php ├── prsdm-limit-login-attempts.php └── uninstall.php

يحتوي كل ملف على فئة واحدة. تتم تسمية الملفات بعد الفئات التي تحتوي عليها ، ويتم تسمية الأدلة والأدلة الفرعية بعد مساحات الأسماء (الفرعية).

هناك عدة أنماط معمارية وأنظمة تسمية قد تستخدمها. الأمر متروك لك لاختيار واحد يناسبك ويتناسب مع احتياجات مشروعك. عندما يتعلق الأمر بهيكلة مشروعك ، فإن الشيء المهم هو أن تكون متسقًا .

استنتاج

تهانينا! لقد أكملت سلسلة مقالاتنا حول WordPress والبرمجة الشيئية.

نأمل أن تكون قد تعلمت بعض الأشياء وأن تكون متحمسًا لبدء تطبيق ما تعلمته في مشاريعك الخاصة!

فيما يلي ملخص سريع لما تناولناه في هذه السلسلة:

  • جمع المتطلبات: قررنا ما يجب أن يفعله البرنامج المساعد.
  • التصميم: فكرنا في كيفية هيكلة المكون الإضافي ، والعلاقات بين فئاتنا المحتملة ، ونظرة عامة عالية المستوى على تجريداتنا.
  • التنفيذ: لقد كتبنا الكود الفعلي لبعض الأجزاء الرئيسية من البرنامج المساعد. أثناء القيام بذلك ، قدمنا ​​لك العديد من المفاهيم والمبادئ.

ومع ذلك ، فإننا بالكاد خدشنا سطح ما هو OOP وما يجب أن نقدمه. يتطلب الحصول على مهارة جديدة التمرين ، لذا انطلق وابدأ في بناء مكونات WordPress الإضافية الموجهة للكائنات. ترميز سعيد!

أنظر أيضا

  • WordPress and Object-Oriented Programming - نظرة عامة
  • الجزء 2 - WordPress والبرمجة الموجهة للكائنات: مثال من العالم الحقيقي
  • الجزء 3 - WordPress والبرمجة الموجهة للكائنات: مثال WordPress - تحديد النطاق
  • الجزء 4 - WordPress والبرمجة الموجهة للكائنات: مثال على WordPress - التصميم
  • الجزء الخامس - البرمجة الموجهة للكائنات و WordPress: مثال على WordPress - التنفيذ: قائمة الإدارة
  • الجزء 6 - WordPress والبرمجة الموجهة للكائنات: مثال على WordPress - التنفيذ: تسجيل الأقسام
  • الجزء 7 - WordPress والبرمجة الموجهة للكائنات: مثال على WordPress - التنفيذ: إدارة WordPress Hooks