تحوّل إضافة smallPict لووردبريس الصور المرفوعة إلى WebP و AVIF وتقدّمها للزوار. عندما ننشئ وسم إصدار، يرسل GitHub Actions الإصدار عبر commit إلى مستودع SVN في WordPress.org، ومن هناك يصل إلى كل موقع ثُبّتت عليه الإضافة. والمواقع التي فعّلت التحديثات التلقائية تثبّته دون أن يراجع أحد التغيير. تعلن الإضافة Requires PHP: 7.4 و Requires at least: 5.9، لذا يجب أن يعمل البناء نفسه على استضافة مشتركة ما زالت على PHP 7.4 وعلى حاوية تعمل بـ PHP 8.5.
قد يُسقط خطأ فادح (fatal error) في إضافة لوحة wp-admin بالكامل. وقيمة غير مُهرَّبة (unescaped) في صفحة إعدادات تعني ثغرة XSS على كل المواقع في وقت واحد. وغياب التحقق من nonce يجعل إجراءً إدارياً هدفاً لهجوم CSRF. لا يمكن إصلاح أي من هذه المشكلات بصمت: يحتاج الإصلاح إلى إصدار جديد، وتبقى بعض المواقع على النسخة المعطوبة حتى يصلها.
يشرح هذا المقال خط CI/CD لإضافتنا على ووردبريس: معايير WordPress Coding Standards (WPCS) و PHPStan والاختبارات الآلية التي تعمل على سبعة إصدارات من PHP، وبوابة الفحص قبل النشر التي تسبق WordPress.org. كل ما يلي مأخوذ من المستودع smallpict-wp عند الإصدار 1.2.9.
أهم النقاط
- كل pull request إلى
masterيشغّل WPCS و PHPStan و 9 سكربتات اختبار آلية على PHP 7.4 و 8.0 و 8.1 و 8.2 و 8.3 و 8.4 و 8.5 بالتوازي في GitHub Actions.- يفرض WPCS تعقيم المدخلات وتهريب المخرجات والتحقق من nonce والاستعلامات المُعدّة (prepared queries). ويكتشف PHPStan على المستوى 5 أخطاء الأنواع والفروع الميتة انطلاقاً من stubs ووردبريس، دون تشغيل ووردبريس.
- وسم الإصدار يشغّل WPCS مجدداً على إصدارات PHP السبعة كلها، ولا تبدأ المهمة التي ترسل commit إلى SVN في WordPress.org إلا بعد نجاحها جميعاً.
خط CI/CD لإضافة ووردبريس في لمحة
يحمي الإضافةَ مسارا عمل (workflows) في GitHub Actions.
| مسار العمل | يعمل عند | المهام | الفحوص |
|---|---|---|---|
phpcs.yml (PHP Code Quality & Standards) | push إلى master أو develop، و pull requests إلى master | مهمة لكل إصدار PHP: 7.4 و 8.0 و 8.1 و 8.2 و 8.3 و 8.4 و 8.5 | WPCS و PHPStan والاختبارات الآلية |
deploy-all.yml (Deploy to WordPress.org) | push لوسم v* | WPCS على كل إصدار من إصدارات PHP السبعة، ثم deploy | WPCS، ثم البناء و commit إلى SVN |
ملاحظة حول تغطية إصدارات PHP: تعرض لقطات الشاشة في هذا المقال عمليات تشغيل CI السابقة لدينا التي غطّت PHP من 7.4 إلى 8.3. أما اليوم فقد توسعت مصفوفة الاختبار لدينا في GitHub Actions لتغطي PHP من 7.4 إلى PHP 8.5، بما يضمن توافقاً مستمراً مع بيئات تشغيل PHP الحديثة دون أي تراجع على البيئات القديمة.
كل الأدوات تبعيات تطوير (dev dependencies) في Composer، فلا يصل أي منها إلى المستخدمين:
"require-dev": { "wp-coding-standards/wpcs": "^3.4.1", "phpcsstandards/php_codesniffer": "^3.0", "phpstan/phpstan": "^2.1", "szepeviktor/phpstan-wordpress": "^2.0 || ^1.3.5", "phpstan/extension-installer": "^1.4"}اختبار مصفوفة إصدارات PHP المتعددة في GitHub Actions
ما الذي يتغير بين PHP 7.4 و PHP 8.5
يشغّل PHP 7.4 و PHP 8.5 الشيفرة المصدرية نفسها للإضافة بطريقتين مختلفتين. ومن التغييرات التي تمس شيفرة إضافات ووردبريس:
- في PHP 8.0 صارت كثير من الدوال الداخلية ترمي
TypeErrorعند تمرير وسيط من نوع خاطئ، بينما كان 7.4 يعيدnullمع تحذير. الشيفرة التي كانت تمرر مصفوفة إلىstrlen()استمرت في العمل على 7.4 وتتوقف على 8.0. - غيّر PHP 8.0 طريقة مقارنة الرقم بسلسلة نصية غير رقمية. فالتعبير
0 == "abc"قيمتهtrueعلى 7.4 وfalseعلى 8.0، ما يقلب بصمت نتيجة بعض المقارنات غير الصارمة على قيم الخيارات (options). - أضاف PHP 8.0 الدالة
str_starts_with()ودوالاً مرتبطة بها. معالج الصور في إضافتنا يستدعيstr_starts_with(). ولا يعمل ذلك على PHP 7.4 إلا لأن ووردبريس 5.9 وما بعده يتضمن polyfill لها، وهو ما يضمنهRequires at least: 5.9لدينا. - جعل PHP 8.1 تمرير
nullإلى المعاملات التي لا تقبل null في الدوال الداخلية سلوكاً مهملاً (deprecated). التعبيرstrtolower( wp_parse_url( $url, PHP_URL_HOST ) )سليم على 7.4، لكنه يسجّل تنبيه إهمال (deprecation) على 8.1 عندما يكون$urlنسبياً والمضيفnull. وبعض الاستضافات تطبع تنبيهات الإهمال داخل الصفحة. - جعل PHP 8.2 الخصائص الديناميكية (dynamic properties) مهملة. الإسناد إلى خاصية غير مُعلنة في أحد أصنافك يسجّل تنبيه إهمال في كل طلب.
لا يظهر أي من هذه الفروق إلا عند تحليل الشيفرة أو تنفيذها على إصدار PHP الذي يتصرف بشكل مختلف. لهذا يشغّل خط CI/CD كل pull request إلى master على الإصدارات السبعة دفعة واحدة.
مصفوفة GitHub Actions

الشكل 1: مسار العمل PHP Code Quality & Standards عند دمج عمل الإصدار 1.2.0 في master. لكل إصدار PHP مهمة مستقلة، وتعرض مهمة PHP 8.3 الخطوات التي تنفذها كل مهمة: PHPCS ثم PHPStan ثم الاختبارات الآلية.
ملاحظة حول تغطية إصدارات PHP: تعرض لقطات الشاشة أعلاه عمليات تشغيل CI السابقة لدينا التي غطّت PHP من 7.4 إلى 8.3. أما اليوم فقد توسعت مصفوفة الاختبار لدينا في GitHub Actions لتغطي PHP من 7.4 إلى PHP 8.5، بما يضمن توافقاً مستمراً مع بيئات تشغيل PHP الحديثة دون أي تراجع على البيئات القديمة.
jobs: code-quality: name: WPCS & PHPStan (PHP ${{ matrix.php-version }}) runs-on: ubuntu-26.04 strategy: matrix: php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer:v2, cs2pr ini-values: memory_limit=2G coverage: none - run: composer install --prefer-dist --no-progress --no-interaction - name: Run PHPCS (WordPress Coding Standards) run: vendor/bin/phpcs -q --standard=phpcs.xml.dist --report=checkstyle | cs2pr - name: Run PHPStan (WordPress Static Analysis) run: vendor/bin/phpstan analyse --memory-limit=2G - name: Run the standalone tests run: | for t in tests/test_*.php; do echo "== $t" php "$t" || exit 1 doneيبقى fail-fast على قيمته الافتراضية true، فعندما يفشل إصدار واحد يلغي GitHub المهام التي ما زالت تعمل. بذلك لا يكلّف الـ pull request المعطوب إلا القليل، مقابل ألا نرى نتيجة كل إصدار في التشغيل نفسه.
ما تثبته المصفوفة وما لا تثبته
لا تستفيد الفحوص الثلاثة من المصفوفة بالقدر نفسه. يحلل PHPStan الشيفرة وفق إصدار PHP الذي يعمل عليه، وتنفذ الاختبارات الشيفرة على ذلك الإصدار، لذا قد يعطي كلاهما نتيجة مختلفة في كل مهمة. أما PHPCS فيعمل على مستوى الرموز (tokens) ويعطي عملياً النتيجة نفسها على الإصدارات السبعة، ودوره في المصفوفة هو أساساً إثبات أن الأدوات تُثبَّت وتعمل على كل إصدار. وفحص الصياغة والدوال مقابل نطاق من إصدارات PHP في تمريرة واحدة هو مهمة PHPCompatibility، وهي غير مدرجة بعد في مجموعة قواعدنا.
أتمتة WordPress Coding Standards (WPCS)
قواعد sniffs الأمنية المهمة لأي إضافة
WPCS مجموعة من sniffs لأداة PHP_CodeSniffer كُتبت لووردبريس. والقواعد الأهم لأمان الإضافات موجودة في مجموعة القواعد WordPress نفسها:
WordPress.Security.ValidatedSanitizedInputيرصد أي قراءة من$_POSTأو$_GETأو$_REQUESTأو$_COOKIEلم يُطبَّق عليها unslash ثم التعقيم (sanitize).WordPress.Security.EscapeOutputيرصد أيechoأوprintلقيمة لم تمر عبر دالة تهريب مثلesc_html()أوesc_attr()أوesc_url().WordPress.Security.NonceVerificationيرصد أي معالج (handler) يقرأ بيانات الطلب دون التحقق من nonce.WordPress.DB.PreparedSQLوPreparedSQLPlaceholdersيرصدان الاستعلام المبني بدمج السلاسل النصية بدلاً من$wpdb->prepare()، والعناصر النائبة (placeholders) التي لا تطابق وسائطها.
مجموعة قواعد WPCS لدينا
يضيف phpcs.xml.dist مجموعتي WordPress-Extra و WordPress-Docs، ويثبّت نطاق النص (text domain) لـ sniffs الترجمة، ويشترط أن تحمل كل دالة وصنف وثابت عام بادئتنا، حتى لا تتعارض أسماء الإضافة مع أسماء إضافة أخرى على الموقع نفسه:
<rule ref="WordPress"> <exclude name="Universal.Arrays.DisallowShortArraySyntax"/></rule><rule ref="WordPress-Extra"/><rule ref="WordPress-Docs"/>
<rule ref="WordPress.WP.I18n"> <properties> <property name="text_domain" type="array"> <element value="smallpict"/> </property> </properties></rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals"> <properties> <property name="prefixes" type="array"> <element value="smallpict"/> <element value="SmallPict"/> <element value="SMALLPICT"/> </property> </properties></rule>معالج AJAX يجتاز WPCS
يستعيد هذا المعالج من الإضافة الوسائط المنقولة إلى التخزين السحابي (offloaded) على دفعات. وكل نقطة تدخل منها البيانات تُعالَج في أسطره الأولى:
public function ajax_restore_offloaded() { if ( ! check_ajax_referer( 'smallpict_restore_offloaded', 'nonce', false ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'forbidden' ), 403 ); } global $wpdb; $after = isset( $_POST['after'] ) ? absint( wp_unslash( $_POST['after'] ) ) : 0; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin action, the rows change on every call. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s AND post_id > %d ORDER BY post_id ASC LIMIT %d", '_smallpict_offloaded', '1', $after, self::RESTORE_BATCH ) ); // ...}نتحقق من nonce والصلاحية (capability) معاً، لأن nonce الصالح يثبت أن الطلب جاء من صفحة الإدارة لدينا، لا أن المستخدم مسموح له بتنفيذ الإجراء. ونطبّق unslash على $_POST['after'] ونحوّله بـ absint() قبل أن يصل إلى الاستعلام، ومع ذلك يمر الاستعلام عبر $wpdb->prepare() رغم أن القيمة عدد صحيح أصلاً.
الاستثناءات تحتاج إلى اسم sniff وسبب
سطر phpcs:ignore أعلاه مقصود. يحذّر WPCS من استعلامات قاعدة البيانات المباشرة دون تخزين مؤقت للكائنات (object caching)، وهي نصيحة جيدة لعمليات القراءة في الواجهة الأمامية وخاطئة لإجراء إداري تتغير صفوفه مع كل استدعاء. قاعدتنا أن كل ignore يسمي الـ sniff بدقة ويذكر السبب بعد --. ولا يمر تعليق // phpcs:ignore المجرد من المراجعة.
في CI، يكتب PHPCS تقرير checkstyle، ويحوّله cs2pr إلى تعليقات توضيحية (annotations) على diff الـ pull request، فيرى المراجع كل مخالفة على السطر الذي سببها بدلاً من البحث عنها في سجل.
اكتشاف أخطاء المنطق بالتحليل الساكن عبر PHPStan
يقرأ WPCS الشيفرة سطراً بسطر. فهو يعرف أن echo يفتقر إلى استدعاء تهريب، لكنه لا يعرف نوع القيمة في متغير ولا ما إذا كان فرع ما يمكن أن يُنفَّذ أصلاً. يبني PHPStan نموذجاً للأنواع التي تتدفق عبر كل دالة ويبلّغ عن الشيفرة التي تناقضه: استدعاء تابع (method) غير موجود، أو تمرير string|false حيث يلزم string، أو مقارنة قيم لا يمكن أن تتساوى أبداً، أو شروط صحيحة دائماً أو خاطئة دائماً.
استخدام PHPStan مع ووردبريس دون تشغيل ووردبريس
توفر szepeviktor/phpstan-wordpress لـ PHPStan تواقيع دوال وأصناف نواة ووردبريس عبر ملفات stub، إضافة إلى بعض امتدادات الأنواع الخاصة بووردبريس. وتسجّلها phpstan/extension-installer تلقائياً. يقرأ PHPStan هذه التصريحات ولا يحمّل ووردبريس ولا قاعدة بيانات أبداً.
إعدادات PHPStan لدينا
parameters: level: 5 paths: - smallpict.php - includes/ - admin/ treatPhpDocTypesAsCertain: false bootstrapFiles: - phpstan-bootstrap.phpيفحص المستوى 5 نوع كل وسيط يُمرَّر إلى دالة أو تابع، فوق فحوص المستويات الأدنى للأصناف والتوابع والخصائص غير المعروفة وأنواع القيم المعادة والشيفرة الميتة. يطلب treatPhpDocTypesAsCertain: false من PHPStan ألا يعامل كتل @param في docblocks الخاصة بنا كدليل قاطع. فووردبريس يمرر ما يعيده الفلتر أياً كان، لذا قد يصل معامل موثّق على أنه int في صورة سلسلة نصية من إضافة أخرى، ونريد أن يُبقي PHPStan الفحوص الدفاعية التي تعالج هذه الحالة. يعرّف ملف bootstrap ثوابت الإضافة (SMALLPICT_PATH و SMALLPICT_URL وغيرها)، التي تُضبط عادة أثناء التشغيل.
![سجل التحليل الساكن بـ PHPStan لإضافة WordPress في GitHub Actions: خطوة WPCS نظيفة، و PHPStan حلّل 11 من 11 ملفاً في الإضافة بنتيجة "[OK] No errors"](/blog-assets/02_phpcs_phpstan_log.png)
الشكل 2: خطوتا PHPCS و PHPStan في مهمة PHP 8.3. لا يطبع PHPCS شيئاً حين لا يوجد ما يبلّغ عنه. ويحلل PHPStan ملفات الإضافة الـ 11 (ملف الإضافة الرئيسي والأصناف الـ 10 في includes/) ولا يبلّغ عن أي خطأ.
اكتشاف حقيقي من PHPStan: السطر 626 في معالج الصور
في 2 أكتوبر 2026 فشل خط CI/CD عند PHPStan بخطأ واحد:

الشكل 3: توقفت مهمة PHP 8.2 عند خطوة PHPStan مع booleanAnd.rightAlwaysTrue في السطر 626 من includes/class-smallpict-image-processor.php. في ذلك الوقت كانت المسارات المحللة تضم 9 ملفات من الإضافة.
هذه الشيفرة هي الخطوة الأخيرة في عملية التحويل. عندما يكون إعداد صيغة الإخراج في الإضافة WebP أو AVIF، يصبح الملف المحوَّل هو المرفق (attachment) ويُحذف الملف الأصلي المرفوع لتوفير مساحة القرص. أما الخياران الآخران، "AVIF، مع WebP كنسخة احتياطية" و"الاحتفاظ بالصيغة الأصلية"، فيحتفظان بملفات JPEG وPNG الأصلية. وهذا هو فرع الحذف:
// C. Pure 1-to-1 Mode: Delete raw uncompressed source file to free 100% hosting space.if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) { wp_delete_file( $main_file_path );}كان ادعاء PHPStan أن file_exists( $main_file_path ) صحيح دائماً في تلك النقطة. والسبب موجود قبل 150 سطراً، في بداية التابع نفسه:
$main_file_path = get_attached_file( $attachment_id );if ( ! $main_file_path || ! file_exists( $main_file_path ) ) { return new WP_Error( 'file_missing', 'File not found on disk.' );}لا يكمل التابع إلا إذا كان الملف موجوداً، ويحمل PHPStan هذه الحقيقة حتى السطر 626.
لم نأخذ هذا الاكتشاف على علاته. فالدالة file_exists() تسأل نظام الملفات، وقد تتغير الإجابة بين استدعاءين. يتذكر PHPStan افتراضياً نتيجة مثل هذا الاستدعاء (الخيار rememberPossiblyImpureFunctionValues)، على افتراض أن شيئاً بينهما لم يغيّر القرص. قبل حذف الفحص، تأكدنا من أمرين بقراءة الشيفرة:
- لا شيء بين شرط الحماية (guard) والسطر 626 يحذف الملف المصدر أو ينقله. تكتب عملية التحويل كل مخرَج إلى ملف مؤقت ثم تنقله إلى مسار جديد، واستدعاءات
wp_delete_file()الوحيدة على ذلك المسار تحذف ملفات مؤقتة. أما خطوة النقل إلى التخزين السحابي (offload)، التي تحذف ملفات محلية فعلاً، فتعمل بعد السطر 626. - الدالة
wp_delete_file()تتحمل غياب الملف على أي حال. فهي تمرر المسار عبر الفلترwp_delete_fileوتستدعي@unlink()، التي لا تُحدث ضرراً إذا كان الملف قد حُذف بالفعل.
بعد التأكد من الأمرين، كان الشرط شيفرة ميتة، والشيفرة الميتة في فرع حذف تضلل القارئ التالي فيظن أن الملف قد يختفي في منتصف التابع. فأزاله الإصلاح:
-if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {+if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path ) { wp_delete_file( $main_file_path ); }لم يكن الاكتشاف عطلاً يوقف البرنامج. قيمته كانت في ما كشفه عن التابع: افتراضات الشيفرة ابتعدت عن بنيتها، في الفرع الوحيد الذي يحذف ملفات المستخدمين. والدرس الذي خرجنا به أضيق من "ثق بأداة التحليل". فحين يبلّغ المحلل أن شرطاً على دالة تتعامل مع نظام الملفات أو الشبكة أو الوقت صحيح دائماً أو خاطئ دائماً، يجب أن نفحص يدوياً ما يحدث بين الاستدعاءين قبل حذف الشرط.
الاختبارات الآلية لإضافة ووردبريس دون PHPUnit
تشغّل الخطوة الأخيرة في كل مهمة من مهام المصفوفة الملفات الموجودة في tests/. وهذه ليست اختبارات PHPUnit. كل ملف سكربت PHP عادي يعرّف stubs بسيطة لدوال ووردبريس التي يحتاجها (get_option() و update_option() و sanitize_key() وغيرها)، ثم يحمّل صنف الإضافة الخاضع للاختبار، ويشغّله، وينتهي برمز خروج غير صفري عند أول فشل:
for t in tests/test_.php do echo "== $t" php "$t" exit 1doneلدينا اليوم 9 سكربتات من هذا النوع. وهي تغطي المسارات التي قد يكلّف فيها الخطأ العميلَ بياناته أو ماله: خط تحويل WebP و AVIF وبدائله الاحتياطية، وتقديم الصور، ونقل الملفات إلى التخزين السحابي وحذفها منه، واستعادة الملفات المنقولة على دفعات، وسلوك الإضافة عندما يتوقف حساب عن الدفع، وحصة التخزين، وإلغاء التثبيت. يشغّل اختبار إلغاء التثبيت استعلامات إلغاء التثبيت الحقيقية على قاعدة بيانات SQLite في الذاكرة ويتحقق مما يتبقى:
check( array( '_smallpict_cdn_base', '_smallpict_cdn_key', '_smallpict_offloaded', '_smallpictx', '_wp_attached_file' ) === $meta, 'offload markers and foreign meta remain (got ' . implode( ',', $meta ) . ')');يحمي هذا التحقق (assertion) قراراً من السهل التراجع عنه بالخطأ. فإلغاء تثبيت الإضافة يحذف إعداداتها لكنه يُبقي بيانات post meta التي تسجل الملفات الموجودة في التخزين السحابي فقط، حتى يتمكن الموقع الذي يعيد تثبيت الإضافة من العثور على صوره. وأمر DELETE ... LIKE '_smallpict%' مكتوب دون انتباه سيحذف هذه العلامات، والاختبار يفشل إذا حدث ذلك.
اخترنا السكربتات البسيطة بدلاً من مجموعة اختبارات PHPUnit الخاصة بووردبريس من أجل السرعة وتكلفة الإعداد: فهي لا تحتاج إلى خادم قاعدة بيانات ولا إلى نسخة من ووردبريس، وتعمل السكربتات الـ 9 كلها في أقل من ثانية، لذا لا يضيف تشغيلها على سبعة إصدارات من PHP شيئاً يُذكر إلى الـ pull request. الثمن أن الـ stubs لا تحاكي إلا ما كتبناه فيها. والسلوك الذي يعتمد على نواة ووردبريس الحقيقية، مثل معالجة مكتبة الوسائط لبياناتها الوصفية (metadata)، لا تغطيه هذه الاختبارات.
بوابة الفحص قبل النشر على WordPress.org
تُنشر الإصدارات عبر مسار عمل منفصل. يبدأ عند دفع وسم مثل v1.2.9، والمهمة التي ترسل commit إلى SVN في WordPress.org تعلن اعتمادها على مصفوفة الفحص (lint):
jobs: lint: name: WPCS Check (PHP ${{ matrix.php-version }}) runs-on: ubuntu-26.04 strategy: matrix: php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - run: composer install --prefer-dist --no-progress --no-interaction - name: PHP CodeSniffer Check run: vendor/bin/phpcs --standard=phpcs.xml.dist
deploy: name: Deploy needs: lint runs-on: ubuntu-26.04 permissions: contents: read steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: '7.4' - name: Build WP.org Version run: | chmod +x build-wporg.sh ./build-wporg.sh - name: Deploy to WordPress.org uses: 10up/action-wordpress-plugin-deploy@stable with: generate-zip: false env: SVN_USERNAME: ${{ secrets.SVN_USERNAME }} SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SLUG: smallpict BUILD_DIR: build-wporg/smallpict ASSETS_DIR: .wordpress-orgيعني needs: lint أن مهمة deploy لا تبدأ إلا عندما تنجح مهام lint السبع كلها. إذا فشل WPCS على أي إصدار، يتخطى GitHub مهمة deploy ولا يحدث أي commit إلى SVN. تعمل خطوة البناء على PHP 7.4، أقدم إصدار ندعمه. ولرمز المهمة (token) صلاحية قراءة فقط على المستودع، والكتابة الوحيدة التي ينفذها هي إلى SVN في WordPress.org، ببيانات اعتماد محفوظة كأسرار (secrets) في المستودع.

الشكل 4: مسار العمل Deploy to WordPress.org. تعمل مهام WPCS Check الخمس أولاً، ولا تعمل مهمة Deploy أسفلها إلا بعد نجاحها جميعاً.
تشغّل البوابة WPCS مجدداً على الـ commit الموسوم بدلاً من الوثوق بتشغيل الـ pull request السابق. فالوسم قد يشير إلى أي commit، بما في ذلك commit دُفع مباشرة أو commit لم تكتمل فحوصه، وليس لدى مسار عمل الإصدار طريقة أخرى لمعرفة ما يشحنه.
تشغيل فحوص CI محلياً
تغلّف سكربتات Composer الأوامر نفسها التي يشغّلها CI، لذا يمكن إعادة إنتاج أي فشل قبل الدفع (push):
composer installcomposer lint # vendor/bin/phpcs --standard=phpcs.xml.distcomposer lint-fix # vendor/bin/phpcbf: fixes spacing and formatting sniffscomposer phpstan # vendor/bin/phpstan analysefor t in tests/test_.php do php "$t" break doneلإعادة إنتاج فشل من مهمة واحدة في المصفوفة، شغّل الأوامر نفسها تحت إصدار PHP ذاك، مثلاً باستخدام الصورة الرسمية بعد composer install:
docker run --rm -v "$PWD":/app -w /app php:7.4-cli \ sh -c 'vendor/bin/phpstan analyse --memory-limit=2G && for t in tests/test_*.php; do php "$t" || exit 1; done'لا يصلح phpcbf إلا ما يمكن لـ sniff إصلاحه آلياً. أما ملاحظات التهريب والتعقيم و nonce فتحتاج إلى شخص يقرر أي دالة تناسب السياق، ولا نصلحها تلقائياً.
ما لا يغطيه خط CI/CD بعد
على قائمتنا ثلاثة نواقص:
- تشغّل بوابة ما قبل النشر WPCS فقط. يعمل PHPStan والاختبارات الآلية عند الدفع إلى
masterوdevelopوعلى الـ pull requests، لا على وسم الإصدار. وإضافة الخطوتين إلى مهمةlintفي مسار عمل النشر تسد هذا النقص. - لا تتضمن مجموعة قواعدنا PHPCompatibility. إضافة
phpcompatibility/phpcompatibility-wpمعtestVersionبقيمة7.4-سترصد في تشغيل واحد الصياغة والدوال غير الموجودة في 7.4. - تستخدم الاختبارات stubs بدلاً من تثبيت حقيقي لووردبريس. مهمة تكامل (integration) على ووردبريس 5.9 (الحد الأدنى لدينا) وعلى أحدث إصدار ستغطي سلوك النواة الذي لا تستطيع الـ stubs تغطيته.
الأسئلة الشائعة
لماذا يُعد اختبار المصفوفة على PHP من 7.4 إلى 8.5 ضرورياً لإضافات ووردبريس؟
يوصل WordPress.org الإصدار نفسه إلى كل موقع، وترويسة الإضافة (Requires PHP: 7.4) هي المرشّح الوحيد الذي يحدد إصدارات PHP التي تُثبَّت عليها. بين 7.4 و 8.5 غيّر PHP طريقة تعامل الدوال الداخلية مع أنواع الوسائط الخاطئة، وطريقة مقارنة الأرقام بالسلاسل النصية، والأنماط التي تطلق تنبيهات إهمال، لذا قد تنهار شيفرة صحيحة على إصدار بخطأ فادح أو تسجّل تحذيرات على إصدار آخر. تشغيل CI على إصدار PHP واحد يثبت فقط أن الإضافة تعمل على ذلك الإصدار. أما تشغيل PHPStan والاختبارات على كل إصدار مدعوم فيكشف هذه الفروق قبل أن يصل الإصدار إلى المستخدمين.
هل يغني WPCS عن عمليات التدقيق الأمني التقليدية؟
لا. يفحص WPCS الأنماط في السطر الذي تظهر فيه: متغير طلب يُقرأ دون تعقيم، أو echo دون تهريب، أو معالج دون تحقق من nonce، أو استعلام دون $wpdb->prepare(). لكنه لا يستطيع أن يعرف ما إذا كان المعالج يتحقق من الصلاحية الصحيحة، أو ما إذا كانت دالة التهريب تناسب سياق المخرجات، أو ما إذا كان طلب من جهة الخادم يذهب إلى URL يتحكم فيه المستخدم، أو ما إذا كان بإمكان مستخدم الوصول إلى بيانات مستخدم آخر. كما يمكن لتعليق phpcs:ignore أن يُسكت أي sniff. نتعامل مع WPCS كحد أدنى يمنع الأخطاء المعروفة، ونراجع التغييرات المتعلقة بالأمان يدوياً.
كيف يتعامل PHPStan مع دوال نواة ووردبريس دون تنفيذ الشيفرة؟
لا يشغّل PHPStan الشيفرة التي يحللها أبداً، بل يقرأ التصريحات و docblocks ليبني نموذجاً للأنواع. وبالنسبة إلى ووردبريس، توفر szepeviktor/phpstan-wordpress ملفات stub: تواقيع دوال وأصناف نواة ووردبريس وأنواع معاملاتها وأنواع قيمها المعادة، دون أجسامها. يقرأ PHPStan هذه الـ stubs كأنها ووردبريس، فيستطيع التحقق من أن استدعاء wp_delete_file() أو get_attached_file() يمرر الأنواع الصحيحة ويستقبلها. أما الثوابت التي يعرّفها ووردبريس أو الإضافة أثناء التشغيل فتوضع في ملف bootstrap، يحمّله PHPStan قبل التحليل.
الإضافة متاحة على WordPress.org باسم smallPict. ويشرح دليل التثبيت ومرجع إعدادات الصور التثبيت وإعدادات صيغة الإخراج.