الانتقال إلى المحتوى
smallPict

Engineering18 min read

CI/CD لإضافات WordPress: WPCS و PHPStan و PHP من 7.4 إلى 8.5

CI/CD لإضافة WordPress في GitHub Actions: WPCS و PHPStan واختبارات آلية على PHP من 7.4 إلى 8.5، وبوابة فحص قبل النشر على WordPress.org.

smallPict Engineering

Engineering

تحوّل إضافة 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.5WPCS و PHPStan والاختبارات الآلية
deploy-all.yml (Deploy to WordPress.org)push لوسم v*WPCS على كل إصدار من إصدارات PHP السبعة، ثم deployWPCS، ثم البناء و commit إلى SVN

ملاحظة حول تغطية إصدارات PHP: تعرض لقطات الشاشة في هذا المقال عمليات تشغيل CI السابقة لدينا التي غطّت PHP من 7.4 إلى 8.3. أما اليوم فقد توسعت مصفوفة الاختبار لدينا في GitHub Actions لتغطي PHP من 7.4 إلى PHP 8.5، بما يضمن توافقاً مستمراً مع بيئات تشغيل PHP الحديثة دون أي تراجع على البيئات القديمة.

كل الأدوات تبعيات تطوير (dev dependencies) في Composer، فلا يصل أي منها إلى المستخدمين:

JSON
"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

تشغيل CI لإضافة WordPress في GitHub Actions: اختبار مصفوفة PHP بخمس مهام متوازية لـ WPCS و PHPStan على PHP 7.4 و 8.0 و 8.1 و 8.2 و 8.3، وجميعها ناجحة

الشكل 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 الحديثة دون أي تراجع على البيئات القديمة.

YAML
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 الترجمة، ويشترط أن تحمل كل دالة وصنف وثابت عام بادئتنا، حتى لا تتعارض أسماء الإضافة مع أسماء إضافة أخرى على الموقع نفسه:

XML
<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) على دفعات. وكل نقطة تدخل منها البيانات تُعالَج في أسطره الأولى:

PHP
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 لدينا

YAML
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"

الشكل 2: خطوتا PHPCS و PHPStan في مهمة PHP 8.3. لا يطبع PHPCS شيئاً حين لا يوجد ما يبلّغ عنه. ويحلل PHPStan ملفات الإضافة الـ 11 (ملف الإضافة الرئيسي والأصناف الـ 10 في includes/) ولا يبلّغ عن أي خطأ.

اكتشاف حقيقي من PHPStan: السطر 626 في معالج الصور

في 2 أكتوبر 2026 فشل خط CI/CD عند PHPStan بخطأ واحد:

PHPStan يكتشف خطأً في إضافة WordPress: "Right side of && is always true" في includes/class-smallpict-image-processor.php عند السطر 626، القاعدة booleanAnd.rightAlwaysTrue، وفشل المهمة على PHP 8.2

الشكل 3: توقفت مهمة PHP 8.2 عند خطوة PHPStan مع booleanAnd.rightAlwaysTrue في السطر 626 من includes/class-smallpict-image-processor.php. في ذلك الوقت كانت المسارات المحللة تضم 9 ملفات من الإضافة.

هذه الشيفرة هي الخطوة الأخيرة في عملية التحويل. عندما يكون إعداد صيغة الإخراج في الإضافة WebP أو AVIF، يصبح الملف المحوَّل هو المرفق (attachment) ويُحذف الملف الأصلي المرفوع لتوفير مساحة القرص. أما الخياران الآخران، "AVIF، مع WebP كنسخة احتياطية" و"الاحتفاظ بالصيغة الأصلية"، فيحتفظان بملفات JPEG وPNG الأصلية. وهذا هو فرع الحذف:

PHP
// 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 سطراً، في بداية التابع نفسه:

PHP
$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)، على افتراض أن شيئاً بينهما لم يغيّر القرص. قبل حذف الفحص، تأكدنا من أمرين بقراءة الشيفرة:

  1. لا شيء بين شرط الحماية (guard) والسطر 626 يحذف الملف المصدر أو ينقله. تكتب عملية التحويل كل مخرَج إلى ملف مؤقت ثم تنقله إلى مسار جديد، واستدعاءات wp_delete_file() الوحيدة على ذلك المسار تحذف ملفات مؤقتة. أما خطوة النقل إلى التخزين السحابي (offload)، التي تحذف ملفات محلية فعلاً، فتعمل بعد السطر 626.
  2. الدالة wp_delete_file() تتحمل غياب الملف على أي حال. فهي تمرر المسار عبر الفلتر wp_delete_file وتستدعي @unlink()، التي لا تُحدث ضرراً إذا كان الملف قد حُذف بالفعل.

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

DIFF
-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() وغيرها)، ثم يحمّل صنف الإضافة الخاضع للاختبار، ويشغّله، وينتهي برمز خروج غير صفري عند أول فشل:

Shell
for t in tests/test_.php do  echo "== $t"  php "$t"  exit 1done

لدينا اليوم 9 سكربتات من هذا النوع. وهي تغطي المسارات التي قد يكلّف فيها الخطأ العميلَ بياناته أو ماله: خط تحويل WebP و AVIF وبدائله الاحتياطية، وتقديم الصور، ونقل الملفات إلى التخزين السحابي وحذفها منه، واستعادة الملفات المنقولة على دفعات، وسلوك الإضافة عندما يتوقف حساب عن الدفع، وحصة التخزين، وإلغاء التثبيت. يشغّل اختبار إلغاء التثبيت استعلامات إلغاء التثبيت الحقيقية على قاعدة بيانات SQLite في الذاكرة ويتحقق مما يتبقى:

PHP
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):

YAML
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) في المستودع.

بوابة الفحص قبل النشر في مسار عمل Deploy to WordPress.org على GitHub Actions: نجح WPCS Check على PHP 7.4 و 8.0 و 8.1 و 8.2 و 8.3 قبل تشغيل مهمة Deploy

الشكل 4: مسار العمل Deploy to WordPress.org. تعمل مهام WPCS Check الخمس أولاً، ولا تعمل مهمة Deploy أسفلها إلا بعد نجاحها جميعاً.

تشغّل البوابة WPCS مجدداً على الـ commit الموسوم بدلاً من الوثوق بتشغيل الـ pull request السابق. فالوسم قد يشير إلى أي commit، بما في ذلك commit دُفع مباشرة أو commit لم تكتمل فحوصه، وليس لدى مسار عمل الإصدار طريقة أخرى لمعرفة ما يشحنه.

تشغيل فحوص CI محلياً

تغلّف سكربتات Composer الأوامر نفسها التي يشغّلها CI، لذا يمكن إعادة إنتاج أي فشل قبل الدفع (push):

Shell
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:

Shell
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 بعد

على قائمتنا ثلاثة نواقص:

  1. تشغّل بوابة ما قبل النشر WPCS فقط. يعمل PHPStan والاختبارات الآلية عند الدفع إلى master و develop وعلى الـ pull requests، لا على وسم الإصدار. وإضافة الخطوتين إلى مهمة lint في مسار عمل النشر تسد هذا النقص.
  2. لا تتضمن مجموعة قواعدنا PHPCompatibility. إضافة phpcompatibility/phpcompatibility-wp مع testVersion بقيمة 7.4- سترصد في تشغيل واحد الصياغة والدوال غير الموجودة في 7.4.
  3. تستخدم الاختبارات 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. ويشرح دليل التثبيت ومرجع إعدادات الصور التثبيت وإعدادات صيغة الإخراج.

بقلم smallPict Engineering

Engineering

نبني تحويل الصور والتوصيل عبر CDN للمواقع والتطبيقات.

جرّبه على صورك.

20 MB مجانًا كل شهر. نحو دقيقتين للإعداد على WordPress.

المزيد عن أداء الصور و Core Web Vitals والتخزين السحابي.

جميع المقالات

Web Performance5 min read

لماذا تحتاج مواقع ووردبريس الحديثة إلى تقديم AVIF/WebP المزدوج (وكيفية تقديمه دون تعطيل المتصفحات)

قدّم تنسيق AVIF مع التراجع إلى WebP عبر وسوم HTML5 <picture> لتقليص أثقل صورك وتحسين LCP على الهواتف المحمولة، دون تعطيل المتصفحات القديمة أو استهلاك الحصة السحابية على كل صورة مصغرة.

Cloud & CDN3 min read

SmallPict API/SDK: استخدام CDN الخاص بك (BYOK) أم CDN المُدار، أيهما تختار؟

لا تتضمن Ignite شبكة CDN (تعود النتائج مباشرة إلى تطبيقك)، بينما تتيح لك Velocity و Momentum الاختيار بين تخزين SmallPict وشبكة CDN الخاصة بها (الخيار الافتراضي) أو التخزين وشبكة CDN الخاصين بك. إليك كيف تقرر النموذج الأنسب لمشروعك.

WordPress2 min read

تحسين صور ووردبريس مع CDN: ما الذي تتضمنه الخطط وما هو الاستخدام العادل

تتضمن خطط ووردبريس المدفوعة من SmallPict تسليمًا محليًا عالي السرعة (Starter) أو شبكة CDN عالمية مُدارة اختيارية (Pro و Business). إليك بالضبط ما هو مُضمّن، وما يُعد استخدامًا عادلًا، وكيف يعمل رصيد الاستخدام المشترك عبر مواقع متعددة.