لا تكرر نفسك: أتمتة المهام المتكررة باستخدام WP-CLI

نشرت: 2022-03-11

هل سبق لك أن وجدت نفسك تذهب إلى منطقة إدارة WordPress لتحديث السمات والإضافات و WP core؟ بالطبع لديك. هل تم سؤالك ، "هل يمكنك إنشاء / تحديث / حذف جميع المستخدمين في ملف CSV هذا؟" أنا متأكد من أنك واجهت ذلك أيضًا. هل حاولت ترحيل موقع وتمنيت أن يكون هناك مكون إضافي أو أداة خارجية يمكنك الوصول إليها للقيام بالمهمة؟ أعلم أن لدي!

أتمتة المهام المتكررة باستخدام WP-CLI

هناك أداة قوية للغاية متاحة لمساعدتك في هذه المهام والمزيد. قبل أن أخبركم عن ذلك ، أود أن أقوم بإعداد حكاية سريعة.

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

الحل: بدلاً من الدخول إلى لوحة الإدارة وحذف المئات (ربما الآلاف) من الاشتراكات يدويًا ، اخترت الوصول إلى إحدى أدوات WordPress المفضلة ، WP-CLI ، والتي أصلحت المشكلة في بضع ضغطات على المفاتيح.

في هذا المنشور ، أود تقديمك إلى WP-CLI (على افتراض أنك لست صديقًا مقربًا بالفعل) ، وإرشادك عبر أمر مخصص بسيط كتبته لهذا الموقف بالذات ، وأعطيك بعض الأفكار والموارد لاستخدام WP-CLI في التنمية الخاصة بك.

ما هو WP-CLI؟

إذا لم تكن قد سمعت عن WP-CLI من قبل ، فأنت لست وحدك. يبدو أن المشروع ، على الرغم من أن عمره عدة سنوات ، طار تحت رادار WordPress لفترة من الوقت. فيما يلي وصف موجز لماهية WP-CLI وما يفعله من الموقع الرسمي:

WP-CLI عبارة عن مجموعة من أدوات سطر الأوامر لإدارة تثبيتات WordPress. يمكنك تحديث المكونات الإضافية ، وإعداد عمليات تثبيت متعددة المواقع وغير ذلك الكثير ، دون استخدام متصفح الويب.

توضح لك الأوامر التالية قوة WP-CLI خارج الصندوق:

  • wp plugin update --all جميع تحديثات جميع المكونات الإضافية القابلة للتحديث.
  • يقوم wp db export بتصدير ملف تفريغ SQL لقاعدة البيانات الخاصة بك.
  • تقوم wp media regenerate الصور المصغرة للمرفقات (على سبيل المثال ، بعد تغيير الحجم في قالبك).
  • يتحقق wp checksum core من أن ملفات WordPress الأساسية لم يتم العبث بها.
  • يقوم wp search-replace بالبحث عن السلاسل الموجودة في قاعدة البيانات واستبدالها.

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

هل اقتنعت؟ على استعداد للبدء؟ رائعة!

ستحتاج إلى تثبيت WP-CLI مع WordPress الخاص بك (أو بشكل عام على جهازك المحلي). إذا لم تقم بعد بتثبيت WP-CLI على بيئة التطوير المحلية الخاصة بك ، فيمكن العثور على إرشادات التثبيت على موقع الويب هنا. إذا كنت تستخدم Varying Vagrant Vagrants (VVV2) ، فسيتم تضمين WP-CLI. يحتوي العديد من مزودي الاستضافة أيضًا على WP-CLI مدرج في نظامهم الأساسي. سأفترض أنك قمت بتثبيت هذا بنجاح المضي قدمًا.

استخدام WP-CLI لحل المشكلة

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

  1. سنكون قادرين على إيقاف تشغيل الأمر المخصص إذا لم نكن بحاجة إليه
  2. يمكننا بسهولة توسيع أوامرنا وأوامرنا الفرعية مع الحفاظ على الأشياء معيارية.
  3. يمكننا الحفاظ على الوظائف عبر السمات وحتى عمليات تثبيت WordPress الأخرى.

إنشاء البرنامج المساعد

لإنشاء مكون إضافي ، نحتاج إلى إضافة دليل إلى دليل /plugins في دليل wp-content . يمكننا استدعاء هذا الدليل toptal-wpcli . ثم أنشئ ملفين في هذا الدليل:

  • index.php ، الذي يجب أن يحتوي على سطر واحد فقط من التعليمات البرمجية: <?php // Silence is golden
  • plugin.php ، حيث سيذهب كودنا (يمكنك تسمية هذا الملف ما تريد.)

افتح ملف plugin.php وأضف الكود التالي:

 <?php /** * Plugin Name: TOPTAL WP-CLI Commands * Version: 0.1 * Plugin URI: https://n8finch.com/ * Description: Some rando wp-cli commands to make life easier... * Author: Nate Finch * Author URI: https://n8finch.com/ * Text Domain: toptal-wpcli * Domain Path: /languages/ * License: GPL v3 */ /** * NOTE: THIS PLUGIN FILE WILL NOT WORK IN PRODUCTION AS IS AND IS ONLY FOR DEMONSTRATION PURPOSES! * You can of course take the code and repurpose it:-). */ if ( !defined( 'WP_CLI' ) && WP_CLI ) { //Then we don't want to load the plugin return; }

هناك جزءان لهذه الأسطر العديدة الأولى.

أولاً ، لدينا رأس البرنامج المساعد. يتم سحب هذه المعلومات إلى صفحة مسؤول WordPress Plugins وتسمح لنا بتسجيل المكون الإضافي الخاص بنا وتفعيله. مطلوب فقط اسم المكون الإضافي ، ولكن يجب علينا تضمين الباقي لأي شخص قد يرغب في استخدام هذا الرمز (بالإضافة إلى ذواتنا المستقبلية!).

ثانيًا ، نريد التحقق من تعريف WP-CLI. أي أننا نتحقق لمعرفة ما إذا كان ثابت WP-CLI موجودًا. إذا لم يكن الأمر كذلك ، فنحن نريد إنقاذ المكون الإضافي وعدم تشغيله. إذا كان موجودًا ، فنحن واضحون لتشغيل بقية التعليمات البرمجية الخاصة بنا.

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

إضافة الأمر المخصص

بعد ذلك ، نريد تضمين الكود التالي:

 class TOPTAL_WP_CLI_COMMANDS extends WP_CLI_Command { function remove_user() { echo "\n\n hello world \n\n"; } } WP_CLI::add_command( 'toptal', 'TOPTAL_WP_CLI_COMMANDS' );

تؤدي كتلة التعليمات البرمجية هذه إلى شيئين بالنسبة إلينا:

  1. تحدد الفئة TOPTAL_WP_CLI_COMMANDS ، والتي يمكننا تمرير الوسائط إليها.
  2. يقوم بتعيين الأمر toptal للفصل ، حتى نتمكن من تشغيله من سطر الأوامر.

الآن ، إذا قمنا بتنفيذ wp toptal remove_user ، فسنرى:

 $ wp toptal hello hello world

هذا يعني أن الأمر toptal مسجل وأن أمرنا الفرعي remove_user يعمل.

إعداد المتغيرات

نظرًا لأننا نعالج عمليات إزالة المستخدمين بشكل مجمّع ، فنحن نريد إعداد المتغيرات التالية:

 // Keep a tally of warnings and loops $total_warnings = 0; $total_users_removed = 0; // If it's a dry run, add this to the end of the success message $dry_suffix = ''; // Keep a list of emails for users we may want to double check $emails_not_existing = array(); $emails_without_level = array(); // Get the args $dry_run = $assoc_args['dry-run']; $level = $assoc_args['level']; $emails = explode( ',', $assoc_args['email'] );

القصد من كل من المتغيرات هو كما يلي:

  • total_warnings : سنحسب تحذيرًا إذا كان البريد الإلكتروني غير موجود ، أو إذا كان البريد الإلكتروني غير مرتبط بمستوى العضوية الذي نقوم بإزالته.
  • $total_users_removed : نريد حساب عدد المستخدمين الذين تمت إزالتهم في العملية (انظر التحذير أدناه).
  • $dry_suffix : إذا كانت هذه تجربة تجريبية ، فنحن نريد إضافة صياغة لإشعار النجاح النهائي.
  • $emails_not_existing : يخزن قائمة من رسائل البريد الإلكتروني غير الموجودة.
  • $emails_without_level : يخزن قائمة من رسائل البريد الإلكتروني التي لا تحتوي على المستوى المحدد.
  • $dry_run : قيمة منطقية تُخزِّن ما إذا كان البرنامج النصي يقوم بالتشغيل الجاف (صواب) أم لا (خطأ).
  • $level : عدد صحيح يمثل المستوى المطلوب فحصه وربما إزالته.
  • $email : مجموعة من رسائل البريد الإلكتروني للتحقق من المستوى المحدد. سنقوم بحلقة من خلال هذه المجموعة

مع مجموعة المتغيرات الخاصة بنا ، نحن جاهزون لتشغيل الدالة بالفعل. بطريقة WordPress الحقيقية ، سنقوم بتشغيل حلقة.

كتابة الوظيفة بنفسها

نبدأ بإنشاء حلقة foreach للتنقل بين جميع رسائل البريد الإلكتروني في مصفوفة $emails الخاصة بنا:

 // Loop through emails foreach ( $emails as $email ) { // code coming soon } // end foreach

ثم نضيف فحصًا مشروطًا:

 // Loop through emails foreach ( $emails as $email ) { //Get User ID $user_id = email_exists($email); if( !$user_id ) { WP_CLI::warning( "The user {$email} does not seem to exist." ); array_push( $emails_not_existing, $email ); $total_warnings++; continue; } } // end foreach

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

 $ wp toptal remove_user [email protected] --dry-run Warning: The user [email protected] does not seem to exist.

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

إذا كان البريد الإلكتروني موجودًا ، فسنستخدم المتغيرات $user_id و $level للتحقق مما إذا كان المستخدم لديه حق الوصول إلى المستوى. نقوم بتخزين القيمة المنطقية الناتجة في المتغير $has_level :

 // Loop through emails foreach ( $emails as $email ) { //Get User ID $user_id = email_exists($email); if( !$user_id ) { WP_CLI::warning( "The user {$email} does not seem to exist." ); array_push( $emails_not_existing, $email ); $total_warnings++; continue; } // Check membership level. This is a made up function, but you could write one or your membership plugin probably has one. $has_level = function_to_check_membership_level( $level, $user_id ); } // end foreach

مثل معظم الوظائف في هذا المثال ، فإن هذه function_to_check_membership_level() ملفقة ، ولكن يجب أن تحتوي معظم مكونات العضوية الإضافية على وظائف مساعدة للحصول على هذه المعلومات.

الآن ، سننتقل إلى الإجراء الرئيسي: إزالة المستوى من المستخدم. سنستخدم بنية if/else ، والتي تبدو كالتالي:

 foreach ( $emails as $email ) { // Previous code here... // Check membership level. This is a made up function, but you could write one or your membership plugin probably has one. $has_level = function_to_check_membership_level( $level, $user_id ); if ( $has_level ) { if ( !$dry_run ) { // Deactivate membership level. This is a made up function, but you could write one or your membership plugin probably has one. function_to_deactivate_membership_level( $level, $user_id, 'inactive' ); } WP_CLI::success( "Membership canceled for {$email}, Level {$level} removed" . PHP_EOL ); $total_users_removed++; } else { WP_CLI::warning( "The user {$email} does not have Level = {$level} membership." ); array_push( $emails_without_level, $email ); $total_warnings++; } // We could echo something here to show that things are processing... } // end foreach

إذا كانت قيمة $has_level هي "الحقيقة" ، مما يعني أن المستخدم لديه حق الوصول إلى مستوى العضوية ، فنحن نريد تشغيل وظيفة لإزالة هذا المستوى. في هذا المثال ، سنستخدم function_to_deactivate_membership_level() لتنفيذ هذا الإجراء.

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

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

التشطيب والتقرير

بمجرد انتهاء الحلقة ، نريد تسجيل نتائجنا في وحدة التحكم. إذا كان هذا تشغيلًا جافًا ، فنحن نريد تسجيل رسالة إضافية إلى وحدة التحكم:

 if ( $dry_run ) { $dry_suffix = 'BUT, nothing really changed because this was a dry run:-).'; }

سيتم إلحاق هذه $dry-suffix بالتحذيرات وإشعارات النجاح التي نسجلها بعد ذلك.

في النهاية ، نريد تسجيل نتائجنا كرسالة نجاح وتحذيراتنا كرسائل تحذير. سنفعل ذلك على النحو التالي:

 WP_CLI::success( "{$total_users_removed} User/s been removed, with {$total_warnings} warnings. {$dry_suffix}" ); if ( $total_warnings ) { $emails_not_existing = implode(',', $emails_not_existing); $emails_without_level = implode(',', $emails_without_level); WP_CLI::warning( "These are the emails to double check and make sure things are on the up and up:" . PHP_EOL . "Non-existent emails: " . $emails_not_existing . PHP_EOL . "Emails without the associated level: " . $emails_without_level . PHP_EOL ); }

لاحظ أننا نستخدم أساليب المساعد WP_CLI::success و WP_CLI::warning . يتم توفيرها بواسطة WP-CLI لتسجيل المعلومات إلى وحدة التحكم. يمكنك بسهولة تسجيل السلاسل ، وهو ما نقوم به هنا ، بما في ذلك $total_users_removed $ total_users_removed و $ $total_warnings و $dry_suffix .

أخيرًا ، إذا تراكمت لدينا أي تحذيرات طوال وقت تشغيل البرنامج النصي ، فنحن نريد طباعة هذه المعلومات إلى وحدة التحكم. بعد إجراء فحص شرطي ، نقوم بتحويل متغيرات المصفوفة $ $emails_not_existing $ و $emails_without_level إلى متغيرات سلسلة. نقوم بذلك حتى نتمكن من طباعتها على وحدة التحكم باستخدام طريقة WP_CLI::warning helper.

إضافة وصف

نعلم جميعًا أن التعليقات مفيدة للآخرين ولأنفسنا في المستقبل تعود إلى الأسابيع أو الأشهر أو حتى السنوات اللاحقة. يوفر WP-CLI واجهة للأوصاف القصيرة (shortdesc) والأوصاف الطويلة (longdesc) والتي تسمح لنا بتعليق أمرنا. سنضع في الجزء العلوي من الأمر لدينا ، بعد تحديد فئة TOPTAL_WP_CLI_COMMANDS :

 /** * Remove a membership level from a user * * ## OPTIONS * --level=<number> * : Membership level to check for and remove * * --email=<email> * : Email of user to check against * * [--dry-run] * : Run the entire search/replace operation and show report, but don't save changes to the database. * * ## EXAMPLES * * wp toptal remove_user --level=5 [email protected],[email protected], [email protected] --dry-run * * @when after_wp_load */

في longdesc ، نحدد ما نتوقع أن يستقبله أمرنا المخصص. بناء الجملة الخاص بـ shortdesc و longdesc هو Markdown Extra. ضمن قسم ## OPTIONS ، نحدد الحجج التي نتوقع تلقيها. إذا كانت الوسيطة مطلوبة ، فإننا نلفها في < > ، وإذا كانت اختيارية ، فإننا نلفها في [ ] .

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

 $ wp toptal remove_user --level=5 --dry-run Error: Parameter errors: missing --email parameter (Email of user to check against)

يتضمن قسم ## EXAMPLES مثالاً لما يمكن أن يبدو عليه الأمر عند استدعائه.

أمرنا المخصص اكتمل الآن. يمكنك أن ترى الجوهر النهائي هنا.

تحذير ومجال للتحسين

من المهم مراجعة العمل الذي قمنا به هنا لمعرفة كيف يمكن تحسين الكود وتوسيعه وإعادة بنائه. هناك العديد من مجالات التحسين لهذا البرنامج النصي. فيما يلي بعض الملاحظات حول التحسينات التي يمكن إجراؤها.

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

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

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

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

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

في الخطوة الأخيرة ، بدلاً من طباعة رسائل البريد الإلكتروني إلى وحدة التحكم في الخطوة الأخيرة ، يمكننا أيضًا تصديرها تلقائيًا إلى ملف CSV أو ملف نصي عادي

أخيرًا ، لا توجد عمليات تحقق للتأكد من حصولنا على عدد صحيح لمتغير $level أو قائمة بريد إلكتروني أو قائمة مفصولة بفواصل من رسائل البريد الإلكتروني في متغير $emails . في الوقت الحالي ، إذا قام شخص ما بتضمين السلاسل بدلاً من الأعداد الصحيحة ، أو أسماء تسجيل دخول المستخدم بدلاً من رسائل البريد الإلكتروني ، فلن يعمل البرنامج النصي (ولن يطرح أي أخطاء). يمكن إضافة الشيكات للأعداد الصحيحة ورسائل البريد الإلكتروني.

أفكار لمزيد من الأتمتة ومزيد من القراءة

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

هناك عدة طرق يمكنك من خلالها استخدام WP-CLI. هنا بعض ما افضل:

  • قم بتحديث السمات والإضافات ونواة WP دون الحاجة إلى الدخول إلى لوحة الإدارة.
  • تصدير قواعد البيانات للنسخ الاحتياطي أو إجراء تفريغ SQL سريع إذا كنت أرغب في اختبار استعلام SQL.
  • ترحيل مواقع WordPress.
  • قم بتثبيت مواقع WordPress جديدة ببيانات وهمية أو إعدادات مخصصة لمجموعة المكونات الإضافية.
  • قم بتشغيل المجاميع الاختبارية على الملفات الأساسية للتأكد من عدم تعرضها للخطر. (يوجد بالفعل مشروع قيد التنفيذ لتوسيع هذا إلى السمات والمكونات الإضافية في WP repo.)
  • اكتب البرنامج النصي الخاص بك للتحقق من مضيفات المواقع وتحديثها وصيانتها (التي كتبت عنها هنا).

الاحتمالات مع WP-CLI لا حدود لها. إليك بعض الموارد التي تساعدك على المضي قدمًا:

  • موقع WP-CLI الرئيسي: http://wp-cli.org
  • أوامر WP-CLI: https://developer.wordpress.org/cli/commands/
  • مدونة WP-CLI الرسمية: https://make.wordpress.org/cli/
  • كتيب WP-CLI: https://make.wordpress.org/cli/handbook/
  • في WooCommerce؟ تحقق من WC-CLI: https://github.com/woocommerce/woocommerce/wiki/WC-CLI-Overview#woocommerce-commands
  • مقابلة بودكاست مع دانيال باتشوبر ، المشرف على المشروع: https://howibuilt.it/episode-28-daniel-bachhuber-wp-cli/