ข้ามไปยังเนื้อหา
smallPict

Engineering11 min read

WordPress Plugin CI/CD: WPCS, PHPStan และ PHP 7.4 ถึง 8.5

CI/CD ปลั๊กอิน WordPress บน GitHub Actions: WPCS, PHPStan และเทสต์อัตโนมัติบน PHP 7.4 ถึง 8.5 พร้อมด่านตรวจก่อน deploy ขึ้น WordPress.org

smallPict Engineering

Engineering

ปลั๊กอิน WordPress ของ smallPict แปลงไฟล์ที่อัปโหลดเป็น WebP และ AVIF แล้วส่งให้ผู้เข้าชม เมื่อเราสร้าง tag สำหรับ release ใหม่ GitHub Actions จะ commit ขึ้นที่เก็บ SVN ของ WordPress.org และจากตรงนั้น release จะไปถึงทุกเว็บไซต์ที่ติดตั้งปลั๊กอินไว้ เว็บไซต์ที่เปิดอัปเดตอัตโนมัติจะติดตั้งทันทีโดยไม่มีใครตรวจการเปลี่ยนแปลง ปลั๊กอินประกาศ Requires PHP: 7.4 และ Requires at least: 5.9 ดังนั้น build เดียวต้องทำงานได้ทั้งบน shared hosting ที่ยังใช้ PHP 7.4 และบน container ที่รัน PHP 8.5

fatal error ในปลั๊กอินเพียงจุดเดียวอาจทำให้ wp-admin ใช้งานไม่ได้ ค่าที่ไม่ได้ escape บนหน้าตั้งค่าคือบั๊ก XSS บนทุกเว็บไซต์พร้อมกัน การตรวจ nonce ที่ขาดไปทำให้การกระทำของผู้ดูแลกลายเป็นเป้าของ CSRF ปัญหาเหล่านี้แก้แบบเงียบ ๆ ไม่ได้ การแก้ต้องออก release ใหม่อีกครั้ง และบางเว็บไซต์จะยังใช้เวอร์ชันที่มีปัญหาจนกว่า release นั้นจะไปถึง

บทความนี้อธิบาย pipeline CI/CD ของปลั๊กอิน WordPress ของเรา ได้แก่ WordPress Coding Standards (WPCS), PHPStan และเทสต์อัตโนมัติที่รันบน PHP เจ็ดเวอร์ชัน รวมถึงด่านตรวจก่อน deploy ที่วางไว้หน้า WordPress.org ทุกอย่างด้านล่างมาจาก repository smallpict-wp ที่ release 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 บังคับให้ sanitize อินพุต, escape เอาต์พุต, ใช้ nonce และ prepared query ส่วน PHPStan ที่ level 5 หา type error และ branch ที่ไม่มีวันทำงานจาก stub ของ WordPress โดยไม่ต้องรัน WordPress
  • tag สำหรับ release จะรัน WPCS ซ้ำบน PHP ทั้งเจ็ดเวอร์ชัน และ job ที่ commit ขึ้น SVN ของ WordPress.org จะเริ่มหลังจากทั้งเจ็ดเวอร์ชันผ่านเท่านั้น

ภาพรวม pipeline CI/CD ของปลั๊กอิน WordPress

workflow ของ GitHub Actions สองตัวคอยป้องกันปลั๊กอิน

Workflowรันเมื่อJobการตรวจ
phpcs.yml (PHP Code Quality & Standards)push ไปที่ master หรือ develop, pull request ที่เข้า masterหนึ่ง job ต่อหนึ่งเวอร์ชัน PHP: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5WPCS, PHPStan, เทสต์อัตโนมัติ
deploy-all.yml (Deploy to WordPress.org)push tag v*WPCS บน PHP แต่ละเวอร์ชันจากทั้ง 7 เวอร์ชัน ตามด้วย deployWPCS ตามด้วย build และ commit ขึ้น SVN

หมายเหตุเรื่องเวอร์ชัน PHP ที่ครอบคลุม: ภาพหน้าจอในบทความนี้แสดงการรัน CI ก่อนหน้าของเราที่ครอบคลุม PHP 7.4 ถึง 8.3 ปัจจุบัน test matrix ใน GitHub Actions ของเราขยายให้ครอบคลุม PHP 7.4 ถึง PHP 8.5 แล้ว เพื่อให้เข้ากันได้กับ PHP runtime รุ่นใหม่อย่างต่อเนื่อง โดยไม่มี regression บนสภาพแวดล้อมรุ่นเก่า

เครื่องมือทั้งหมดเป็น dev dependency ของ 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"}

การทดสอบแบบ matrix หลายเวอร์ชัน PHP ใน GitHub Actions

สิ่งที่เปลี่ยนไประหว่าง PHP 7.4 และ PHP 8.5

PHP 7.4 และ PHP 8.5 รันซอร์สโค้ดปลั๊กอินชุดเดียวกันได้ผลต่างกัน การเปลี่ยนแปลงบางส่วนที่กระทบโค้ดปลั๊กอิน WordPress มีดังนี้:

  • PHP 8.0 ทำให้ฟังก์ชันภายในจำนวนมาก throw TypeError เมื่อได้รับอาร์กิวเมนต์ผิดชนิด ขณะที่ 7.4 คืนค่า null พร้อม warning โค้ดที่ส่ง array ให้ strlen() ยังรันต่อได้บน 7.4 แต่หยุดทำงานบน 8.0
  • PHP 8.0 เปลี่ยนวิธีเปรียบเทียบตัวเลขกับสตริงที่ไม่ใช่ตัวเลข 0 == "abc" ได้ true บน 7.4 และ false บน 8.0 ทำให้การเปรียบเทียบแบบหลวมกับค่า option บางจุดให้ผลกลับด้านโดยไม่มีสัญญาณเตือน
  • PHP 8.0 เพิ่ม str_starts_with() และฟังก์ชันที่เกี่ยวข้อง ตัวประมวลผลภาพของเราเรียก str_starts_with() บน PHP 7.4 โค้ดนี้ทำงานได้เพียงเพราะ WordPress 5.9 ขึ้นไปมี polyfill มาให้ ซึ่ง Requires at least: 5.9 ของเรารับประกันไว้
  • PHP 8.1 ประกาศ deprecated การส่ง null ให้พารามิเตอร์แบบ non-nullable ของฟังก์ชันภายใน strtolower( wp_parse_url( $url, PHP_URL_HOST ) ) ไม่มีปัญหาบน 7.4 แต่บน 8.1 จะบันทึก deprecation เมื่อ $url เป็น path แบบสัมพัทธ์และ host เป็น null โฮสต์บางรายพิมพ์ deprecation ออกมาบนหน้าเว็บ
  • PHP 8.2 ประกาศ deprecated พร็อพเพอร์ตีแบบไดนามิก การกำหนดค่าให้พร็อพเพอร์ตีที่ไม่ได้ประกาศไว้ในคลาสของคุณเองจะบันทึก deprecation ทุก request

ปัญหาแต่ละข้อจะปรากฏก็ต่อเมื่อโค้ดถูกวิเคราะห์หรือรันบนเวอร์ชัน PHP ที่มีพฤติกรรมต่างไป นี่คือเหตุผลที่ pipeline รันทุก pull request ที่เข้า master บนทั้งเจ็ดเวอร์ชันพร้อมกัน

Matrix ใน GitHub Actions

การรัน CI ของปลั๊กอิน WordPress ใน GitHub Actions: การทดสอบแบบ matrix ของ PHP ด้วย job WPCS และ PHPStan ห้า job ที่รันขนานกันบน PHP 7.4, 8.0, 8.1, 8.2 และ 8.3 ผ่านทั้งหมด

ภาพที่ 1: workflow PHP Code Quality & Standards ตอน merge งานของ 1.2.0 เข้า master PHP แต่ละเวอร์ชันเป็น job แยกกัน และ job ของ PHP 8.3 แสดงขั้นตอนที่ทุก job รัน ได้แก่ PHPCS, PHPStan แล้วตามด้วยเทสต์อัตโนมัติ

หมายเหตุเรื่องเวอร์ชัน PHP ที่ครอบคลุม: ภาพหน้าจอด้านบนแสดงการรัน CI ก่อนหน้าของเราที่ครอบคลุม PHP 7.4 ถึง 8.3 ปัจจุบัน test matrix ใน GitHub Actions ของเราขยายให้ครอบคลุม PHP 7.4 ถึง PHP 8.5 แล้ว เพื่อให้เข้ากันได้กับ PHP runtime รุ่นใหม่อย่างต่อเนื่อง โดยไม่มี regression บนสภาพแวดล้อมรุ่นเก่า

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 จะยกเลิก job ที่ยังรันอยู่ pull request ที่มีปัญหาจึงใช้ทรัพยากรน้อย แลกกับการที่ไม่เห็นผลของทุกเวอร์ชันในการรันครั้งเดียวกัน

สิ่งที่ matrix พิสูจน์ได้ และสิ่งที่พิสูจน์ไม่ได้

การตรวจทั้งสามแบบได้ประโยชน์จาก matrix ไม่เท่ากัน PHPStan วิเคราะห์โค้ดตามเวอร์ชัน PHP ที่ตัวเองรันอยู่ และเทสต์รันโค้ดบนเวอร์ชันนั้น ทั้งสองจึงให้ผลต่างกันได้ในแต่ละ job ส่วน PHPCS ทำงานระดับ token และในทางปฏิบัติให้ผลเหมือนกันทั้งเจ็ดเวอร์ชัน ใน matrix มันจึงพิสูจน์เป็นหลักว่าชุดเครื่องมือติดตั้งและรันได้บนแต่ละเวอร์ชัน การตรวจ syntax และฟังก์ชันกับช่วงเวอร์ชัน PHP ในการรันครั้งเดียวเป็นหน้าที่ของ PHPCompatibility ซึ่ง ruleset ของเรายังไม่ได้ใส่ไว้

การทำ WordPress Coding Standards (WPCS) ให้เป็นอัตโนมัติ

sniff ด้านความปลอดภัยที่สำคัญสำหรับปลั๊กอิน

WPCS คือชุด sniff ของ PHP_CodeSniffer ที่เขียนมาสำหรับ WordPress กฎที่สำคัญที่สุดต่อความปลอดภัยของปลั๊กอินอยู่ใน ruleset WordPress เอง:

  • WordPress.Security.ValidatedSanitizedInput แจ้งทุกครั้งที่อ่าน $_POST, $_GET, $_REQUEST หรือ $_COOKIE โดยไม่ผ่านการ unslash และ sanitize
  • WordPress.Security.EscapeOutput แจ้งทุกครั้งที่ echo หรือ print ค่าที่ไม่ได้ผ่านฟังก์ชัน escape เช่น esc_html(), esc_attr() หรือ esc_url()
  • WordPress.Security.NonceVerification แจ้ง handler ที่อ่านข้อมูลจาก request โดยไม่ตรวจสอบ nonce
  • WordPress.DB.PreparedSQL และ PreparedSQLPlaceholders แจ้ง query ที่สร้างด้วยการต่อสตริงแทนการใช้ $wpdb->prepare() และ placeholder ที่ไม่ตรงกับอาร์กิวเมนต์

ruleset WPCS ของเรา

phpcs.xml.dist เพิ่ม WordPress-Extra และ WordPress-Docs กำหนด text domain ให้ sniff ด้านการแปล และบังคับให้ฟังก์ชัน คลาส และค่าคงที่ระดับ global ทุกตัวมี prefix ของเรา เพื่อไม่ให้ชื่อในปลั๊กอินชนกับปลั๊กอินอื่นบนเว็บไซต์เดียวกัน:

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 handler ที่ผ่าน WPCS

handler นี้ในปลั๊กอินกู้คืนสื่อที่ offload ไปแล้วเป็นชุด ๆ ขอบเขตอินพุตทุกจุดถูกจัดการในบรรทัดแรก ๆ ของฟังก์ชัน:

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 ที่ถูกต้องพิสูจน์ได้แค่ว่า request มาจากหน้าผู้ดูแลของเรา ไม่ได้พิสูจน์ว่าผู้ใช้มีสิทธิ์ทำการกระทำนั้น $_POST['after'] ถูก unslash และแปลงด้วย absint() ก่อนไปถึง query และ query ยังผ่าน $wpdb->prepare() แม้ค่าจะเป็นจำนวนเต็มอยู่แล้วก็ตาม

การ suppress ต้องระบุชื่อ sniff และเหตุผล

บรรทัด phpcs:ignore ด้านบนตั้งใจใส่ไว้ WPCS เตือนเรื่อง query ฐานข้อมูลโดยตรงที่ไม่มี object cache ซึ่งเป็นคำแนะนำที่ดีสำหรับการอ่านข้อมูลฝั่ง front-end แต่ไม่เหมาะกับการกระทำของผู้ดูแลที่แถวข้อมูลเปลี่ยนทุกครั้งที่เรียก กฎของเราคือทุก ignore ต้องระบุชื่อ sniff ให้ตรงและบอกเหตุผลหลัง -- ส่วน // phpcs:ignore ที่ไม่มีอะไรต่อท้ายจะไม่ผ่าน review

ใน CI นั้น PHPCS เขียนรายงานแบบ checkstyle แล้ว cs2pr แปลงเป็น annotation บน diff ของ pull request ผู้รีวิวจึงเห็นการละเมิดแต่ละจุดบนบรรทัดที่ก่อให้เกิดปัญหา แทนที่จะต้องไปหาใน log

จับข้อบกพร่องเชิงตรรกะด้วย static analysis ของ PHPStan

WPCS อ่านโค้ดทีละบรรทัด มันรู้ว่า echo ขาดการเรียก escape แต่ไม่รู้ว่าตัวแปรเก็บค่าชนิดอะไร หรือ branch หนึ่งจะมีโอกาสทำงานหรือไม่ PHPStan สร้างแบบจำลองของชนิดข้อมูลที่ไหลผ่านแต่ละฟังก์ชัน แล้วรายงานโค้ดที่ขัดกับแบบจำลองนั้น เช่น การเรียก method ที่ไม่มีอยู่ การส่ง string|false ไปในที่ที่ต้องการ string การเปรียบเทียบค่าที่ไม่มีวันเท่ากัน และเงื่อนไขที่เป็นจริงเสมอหรือเท็จเสมอ

ใช้ PHPStan กับ WordPress โดยไม่ต้องรัน WordPress

szepeviktor/phpstan-wordpress ให้ signature ของฟังก์ชันและคลาสใน WordPress core แก่ PHPStan ผ่านไฟล์ stub พร้อม type extension เฉพาะของ WordPress อีกเล็กน้อย phpstan/extension-installer ลงทะเบียนให้อัตโนมัติ PHPStan อ่านการประกาศเหล่านั้นและไม่เคยโหลด WordPress หรือฐานข้อมูล

การตั้งค่า PHPStan ของเรา

YAML
parameters:    level: 5    paths:        - smallpict.php        - includes/        - admin/    treatPhpDocTypesAsCertain: false    bootstrapFiles:        - phpstan-bootstrap.php

Level 5 ตรวจชนิดของทุกอาร์กิวเมนต์ที่ส่งให้ฟังก์ชันหรือ method เพิ่มเติมจากการตรวจของ level ที่ต่ำกว่า ได้แก่ คลาส method และพร็อพเพอร์ตีที่ไม่รู้จัก ชนิดของค่าที่คืน และ dead code ส่วน treatPhpDocTypesAsCertain: false บอก PHPStan ไม่ให้ถือว่า docblock @param ที่เราเขียนเองเป็นข้อพิสูจน์ WordPress ส่งต่อค่าอะไรก็ตามที่ filter คืนมา พารามิเตอร์ที่ระบุใน docblock ว่าเป็น int จึงอาจได้รับสตริงจากปลั๊กอินอื่น และเราต้องการให้ PHPStan คงการตรวจเชิงป้องกันที่จัดการกรณีนี้ไว้ ไฟล์ bootstrap กำหนดค่าคงที่ของปลั๊กอิน (SMALLPICT_PATH, SMALLPICT_URL และอื่น ๆ) ซึ่งปกติจะถูกตั้งค่าตอน runtime

log static analysis ของ PHPStan สำหรับ WordPress ใน GitHub Actions: ขั้นตอน WPCS ผ่านโดยไม่มีปัญหา PHPStan วิเคราะห์ไฟล์ปลั๊กอินครบ 11 จาก 11 ไฟล์และแสดง "[OK] No errors"

ภาพที่ 2: ขั้นตอน PHPCS และ PHPStan ของ job PHP 8.3 PHPCS ไม่พิมพ์อะไรออกมาเมื่อไม่มีอะไรต้องรายงาน PHPStan วิเคราะห์ไฟล์ปลั๊กอิน 11 ไฟล์ (ไฟล์หลักของปลั๊กอินและคลาส 10 ตัวใน includes/) และไม่พบข้อผิดพลาด

สิ่งที่ PHPStan พบจริง: บรรทัด 626 ของตัวประมวลผลภาพ

วันที่ 2 ตุลาคม 2026 pipeline ล้มเหลวที่ PHPStan ด้วยข้อผิดพลาดหนึ่งรายการ:

PHPStan จับบั๊กในปลั๊กอิน WordPress: "Right side of && is always true" ที่ includes/class-smallpict-image-processor.php บรรทัด 626 กฎ booleanAnd.rightAlwaysTrue job ล้มเหลวบน PHP 8.2

ภาพที่ 3: job ของ PHP 8.2 หยุดที่ขั้นตอน PHPStan ด้วย booleanAnd.rightAlwaysTrue ที่บรรทัด 626 ของ includes/class-smallpict-image-processor.php ขณะนั้นปลั๊กอินมี 9 ไฟล์ใน path ที่ถูกวิเคราะห์

โค้ดนี้คือขั้นตอนสุดท้ายของการแปลงไฟล์ เมื่อการตั้งค่า รูปแบบผลลัพธ์ ของปลั๊กอินเป็น WebP หรือ AVIF ไฟล์ที่แปลงแล้วจะกลายเป็น attachment และไฟล์ที่อัปโหลดต้นฉบับจะถูกลบเพื่อประหยัดพื้นที่ดิสก์ ตัวเลือกอื่นคือ "AVIF โดยมี WebP เป็นตัวสำรอง" และ "คงรูปแบบต้นฉบับไว้" จะเก็บไฟล์ JPEG และ PNG ต้นฉบับไว้ นี่คือ branch ที่ลบไฟล์:

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 บรรทัด ที่ส่วนบนสุดของ method เดียวกัน:

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.' );}

method จะทำงานต่อเมื่อไฟล์มีอยู่เท่านั้น และ PHPStan นำข้อเท็จจริงนี้ไปจนถึงบรรทัด 626

เราไม่ได้เชื่อผลนี้ทันที file_exists() ถามระบบไฟล์ และคำตอบอาจเปลี่ยนได้ระหว่างการเรียกสองครั้ง โดยค่าเริ่มต้น PHPStan จะจำผลของการเรียกแบบนี้ไว้ (ตัวเลือก rememberPossiblyImpureFunctionValues) โดยสมมติว่าไม่มีอะไรระหว่างนั้นเปลี่ยนแปลงดิสก์ ก่อนลบการตรวจนี้ เราอ่านโค้ดและยืนยันสองเรื่อง:

  1. ไม่มีโค้ดใดระหว่าง guard กับบรรทัด 626 ที่ลบหรือย้ายไฟล์ต้นทาง การแปลงเขียนเอาต์พุตแต่ละไฟล์ลงไฟล์ชั่วคราวแล้วย้ายไปยัง path ใหม่ และการเรียก wp_delete_file() บน path นั้นมีไว้ลบไฟล์ชั่วคราวเท่านั้น ส่วนขั้นตอน offload ซึ่งลบไฟล์ในเครื่องจริง ๆ รันหลังบรรทัด 626
  2. wp_delete_file() รับมือกับไฟล์ที่ไม่มีอยู่ได้อยู่แล้ว ฟังก์ชันนี้ส่ง path ผ่าน filter wp_delete_file แล้วเรียก @unlink() ซึ่งไม่ก่อความเสียหายใด ๆ เมื่อไฟล์หายไปแล้ว

เมื่อยืนยันทั้งสองข้อแล้ว เงื่อนไขนั้นคือ dead code และ dead code ใน branch ที่ลบไฟล์จะทำให้คนที่อ่านโค้ดต่อเข้าใจผิดว่าไฟล์อาจหายไประหว่างที่ method ทำงาน การแก้ไขจึงลบเงื่อนไขนี้ออก:

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 ); }

สิ่งที่พบไม่ใช่ crash คุณค่าของมันอยู่ที่สิ่งที่บอกเกี่ยวกับ method นี้ สมมติฐานของโค้ดเคลื่อนออกจากโครงสร้างของโค้ด ใน branch เดียวที่ลบไฟล์ของผู้ใช้ บทเรียนที่เราได้แคบกว่า "จงเชื่อเครื่องมือวิเคราะห์" รายงานว่าเป็นจริงเสมอหรือเท็จเสมอบนฟังก์ชันด้านระบบไฟล์ เครือข่าย หรือเวลา ควรได้รับการตรวจด้วยตนเองว่าเกิดอะไรขึ้นระหว่างการเรียกสองครั้ง ก่อนจะลบเงื่อนไขทิ้ง

เทสต์อัตโนมัติสำหรับปลั๊กอิน WordPress โดยไม่ใช้ PHPUnit

ขั้นตอนสุดท้ายของแต่ละ job ใน matrix รันไฟล์ใน tests/ ไฟล์เหล่านี้ไม่ใช่เทสต์ PHPUnit แต่ละไฟล์เป็นสคริปต์ PHP ธรรมดาที่กำหนด stub ขั้นต่ำของฟังก์ชัน WordPress ที่ต้องใช้ (get_option(), update_option(), sanitize_key() และอื่น ๆ) โหลดคลาสของปลั๊กอินที่จะทดสอบ เรียกใช้งาน และจบด้วย exit code ที่ไม่ใช่ศูนย์ทันทีที่เจอความล้มเหลวครั้งแรก:

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

ปัจจุบันมีสคริปต์แบบนี้ 9 ตัว ครอบคลุมเส้นทางที่ความผิดพลาดทำให้ลูกค้าเสียข้อมูลหรือเสียเงิน ได้แก่ pipeline การแปลง WebP และ AVIF พร้อม fallback การส่งภาพ การ offload และลบไฟล์บน cloud storage การกู้คืนไฟล์ที่ offload ไปแล้วเป็นชุด พฤติกรรมเมื่อบัญชีหยุดชำระเงิน โควตาพื้นที่จัดเก็บ และการถอนการติดตั้ง เทสต์การถอนการติดตั้งรัน query ถอนการติดตั้งจริงกับฐานข้อมูล 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 ที่บันทึกว่าไฟล์ใดอยู่บน cloud storage เท่านั้นไว้ เว็บไซต์ที่ติดตั้งปลั๊กอินใหม่จึงยังหาภาพของตัวเองเจอ DELETE ... LIKE '_smallpict%' ที่เขียนอย่างไม่ระวังจะลบเครื่องหมายเหล่านั้นทิ้ง และถ้าเป็นเช่นนั้น เทสต์จะล้มเหลว

เราเลือกสคริปต์ธรรมดาแทนชุดเทสต์ PHPUnit ของ WordPress เพราะความเร็วและต้นทุนการตั้งค่า สคริปต์เหล่านี้ไม่ต้องใช้เซิร์ฟเวอร์ฐานข้อมูลหรือ checkout ของ WordPress และทั้ง 9 ตัวรันเสร็จในเวลาไม่ถึงหนึ่งวินาที การรันบน PHP เจ็ดเวอร์ชันจึงแทบไม่เพิ่มเวลาให้ pull request ข้อแลกเปลี่ยนคือ stub จำลองได้เฉพาะสิ่งที่เราเขียนลงไป พฤติกรรมที่ขึ้นกับ WordPress core จริง เช่น การจัดการ metadata ของคลังสื่อเอง ไม่ได้รับการครอบคลุมจากเทสต์เหล่านี้

ด่านตรวจก่อน deploy ขึ้น WordPress.org

release ถูกส่งออกผ่าน workflow แยกต่างหาก การ push tag เช่น v1.2.9 จะเริ่ม workflow นี้ และ job ที่ commit ขึ้น SVN ของ WordPress.org ประกาศว่าขึ้นอยู่กับ matrix ของ 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 หมายความว่า job deploy จะเริ่มก็ต่อเมื่อ job lint ทั้งเจ็ดสำเร็จ หาก WPCS ล้มเหลวบนเวอร์ชันใดก็ตาม GitHub จะข้าม deploy และไม่มีการ commit ขึ้น SVN ขั้นตอน build รันบน PHP 7.4 ซึ่งเป็นเวอร์ชันเก่าที่สุดที่เรารองรับ token ของ job มีสิทธิ์อ่านอย่างเดียวบน repository การเขียนเพียงอย่างเดียวคือการเขียนขึ้น SVN ของ WordPress.org โดยเก็บข้อมูลรับรองไว้เป็น secret ของ repository

ด่านตรวจก่อน deploy ใน workflow Deploy to WordPress.org ของ GitHub Actions: WPCS Check ผ่านบน PHP 7.4, 8.0, 8.1, 8.2 และ 8.3 ก่อนที่ job Deploy จะรัน

ภาพที่ 4: workflow Deploy to WordPress.org ซึ่ง job WPCS Check ทั้งห้าจะรันก่อน ส่วน job Deploy ด้านล่างจะรันหลังจากทั้งห้าผ่านแล้วเท่านั้น

ด่านตรวจนี้รัน WPCS ซ้ำบน commit ที่ติด tag แทนที่จะเชื่อผลการรันของ pull request ก่อนหน้า tag ชี้ไปที่ commit ใดก็ได้ รวมถึง commit ที่ push ตรงเข้ามาหรือ commit ที่การตรวจยังรันไม่จบ และ workflow ของ release ไม่มีทางอื่นที่จะรู้ว่ากำลังส่งอะไรออกไป

รันการตรวจของ CI บนเครื่อง local

สคริปต์ของ 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

หากต้องการจำลองความล้มเหลวจาก job หนึ่งใน matrix ให้รันคำสั่งเดียวกันภายใต้เวอร์ชัน PHP นั้น เช่น ใช้ image ทางการหลังจากรัน 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 แก้ได้แบบกลไก ปัญหาด้าน escape, sanitize และ nonce ต้องให้คนตัดสินใจว่าฟังก์ชันใดเหมาะกับบริบท และเราไม่แก้ส่วนนี้อัตโนมัติ

สิ่งที่ pipeline ยังไม่ครอบคลุม

มีช่องว่างสามข้ออยู่ในรายการงานของเรา:

  1. ด่านตรวจก่อน deploy รันเฉพาะ WPCS ส่วน PHPStan และเทสต์อัตโนมัติรันเมื่อ push ไปที่ master และ develop และบน pull request แต่ไม่รันบน tag ของ release การเพิ่มทั้งสองขั้นตอนลงใน job lint ของ workflow deploy จะปิดช่องว่างนี้
  2. ruleset ของเรายังไม่มี PHPCompatibility การเพิ่ม phpcompatibility/phpcompatibility-wp โดยตั้ง testVersion เป็น 7.4- จะตรวจพบ syntax และฟังก์ชันที่ไม่มีบน 7.4 ได้ในการรันครั้งเดียว
  3. เทสต์ใช้ stub แทนการติดตั้ง WordPress จริง job แบบ integration ที่รันกับ WordPress 5.9 (เวอร์ชันต่ำสุดที่เรารองรับ) และ release ล่าสุดจะครอบคลุมพฤติกรรมของ core ที่ stub จำลองไม่ได้

FAQ

ทำไมการทดสอบแบบ matrix ตั้งแต่ PHP 7.4 ถึง 8.5 จึงสำคัญมากสำหรับปลั๊กอิน WordPress?

WordPress.org ส่ง release เดียวกันไปยังทุกเว็บไซต์ และ header ของปลั๊กอิน (Requires PHP: 7.4) เป็นตัวกรองเดียวที่กำหนดว่า PHP เวอร์ชันใดจะติดตั้งปลั๊กอินได้ ระหว่าง 7.4 ถึง 8.5 PHP เปลี่ยนวิธีที่ฟังก์ชันภายในจัดการอาร์กิวเมนต์ผิดชนิด วิธีเปรียบเทียบตัวเลขกับสตริง และรูปแบบโค้ดที่ทำให้เกิด deprecation โค้ดที่ถูกต้องบนเวอร์ชันหนึ่งจึงอาจเกิด fatal error หรือบันทึก warning บนอีกเวอร์ชัน การรัน CI บน PHP เวอร์ชันเดียวพิสูจน์ได้เพียงว่าปลั๊กอินทำงานบนเวอร์ชันนั้น การรัน PHPStan และเทสต์บนทุกเวอร์ชันที่รองรับจะจับความแตกต่างเหล่านี้ได้ก่อนที่ release จะไปถึงผู้ใช้

WPCS ใช้แทนการตรวจสอบความปลอดภัยแบบดั้งเดิมได้หรือไม่?

ไม่ได้ WPCS ตรวจรูปแบบโค้ดตรงบรรทัดที่รูปแบบนั้นปรากฏ เช่น ตัวแปรจาก request ที่อ่านโดยไม่ sanitize, echo ที่ไม่ escape, handler ที่ไม่ตรวจ nonce และ query ที่ไม่ใช้ $wpdb->prepare() มันบอกไม่ได้ว่า handler ตรวจ capability ถูกตัวหรือไม่ ฟังก์ชัน escape เหมาะกับบริบทของเอาต์พุตหรือไม่ request ฝั่งเซิร์ฟเวอร์ไปยัง URL ที่ผู้ใช้ควบคุมได้หรือไม่ หรือผู้ใช้คนหนึ่งเข้าถึงข้อมูลของผู้ใช้อีกคนได้หรือไม่ นอกจากนี้ คอมเมนต์ phpcs:ignore ยังปิด sniff ได้ด้วย เรามอง WPCS เป็นมาตรฐานขั้นต่ำที่กันความผิดพลาดที่รู้จักอยู่แล้วออกไป และรีวิวการเปลี่ยนแปลงที่เกี่ยวกับความปลอดภัยด้วยคน

PHPStan จัดการฟังก์ชันของ WordPress core อย่างไรโดยไม่ต้องรันโค้ด?

PHPStan ไม่เคยรันโค้ดที่มันวิเคราะห์ มันอ่านการประกาศและ docblock เพื่อสร้างแบบจำลองของชนิดข้อมูล สำหรับ WordPress นั้น szepeviktor/phpstan-wordpress ให้ไฟล์ stub ซึ่งมี signature ชนิดของพารามิเตอร์ และชนิดของค่าที่คืนของฟังก์ชันและคลาสใน WordPress core โดยไม่มีส่วนเนื้อหาของโค้ด PHPStan อ่าน stub เหล่านั้นเสมือนเป็น WordPress จึงตรวจได้ว่าการเรียก wp_delete_file() หรือ get_attached_file() ส่งและรับชนิดข้อมูลที่ถูกต้อง ค่าคงที่ที่ WordPress หรือปลั๊กอินกำหนดตอน runtime จะใส่ไว้ในไฟล์ bootstrap ซึ่ง PHPStan โหลดก่อนเริ่มวิเคราะห์

ปลั๊กอินนี้อยู่บน WordPress.org ในชื่อ smallPict วิธีติดตั้งและการตั้งค่ารูปแบบผลลัพธ์อยู่ใน คู่มือการติดตั้ง และ เอกสารอ้างอิงการตั้งค่ารูปภาพ

เขียนโดย smallPict Engineering

Engineering

เราสร้างระบบแปลงรูปภาพและการส่งผ่าน CDN สำหรับเว็บไซต์และแอป

ลองกับรูปภาพของคุณเอง

ฟรี 20 MB ต่อเดือน ตั้งค่าบน WordPress ได้ในราวสองนาที

อ่านเพิ่มเติมเรื่องประสิทธิภาพรูปภาพ Core Web Vitals และพื้นที่จัดเก็บบนคลาวด์

บทความทั้งหมด

Web Performance3 min read

ทำไมเว็บไซต์ WordPress ยุคใหม่จึงต้องใช้ AVIF/WebP แบบคู่ (และวิธีส่งมอบโดยไม่ทำให้เบราว์เซอร์แสดงผลผิดพลาด)

ส่งมอบ AVIF พร้อมระบบสำรอง WebP ผ่านแท็ก HTML5 <picture> เพื่อลดขนาดรูปภาพที่หนักที่สุดของคุณและช่วยปรับปรุง LCP บนมือถือ โดยไม่ทำให้เบราว์เซอร์รุ่นเก่าแสดงผลผิดพลาดหรือเสียโควตาคลาวด์ไปกับภาพขนาดย่อทุกภาพ

Cloud & CDN2 min read

SmallPict API/SDK: นำ CDN มาเอง (BYOK) หรือใช้ Managed CDN เลือกแบบไหนดี?

Ignite ไม่มี CDN (ผลลัพธ์ส่งกลับไปยังแอปของคุณโดยตรง) ส่วน Velocity และ Momentum ให้คุณเลือกระหว่างพื้นที่จัดเก็บและ CDN ของ SmallPict (ค่าเริ่มต้น) หรือพื้นที่จัดเก็บและ CDN ของคุณเอง นี่คือวิธีตัดสินใจว่าโมเดลใดเหมาะกับโปรเจกต์ของคุณ

WordPress1 min read

การเพิ่มประสิทธิภาพรูปภาพ WordPress ด้วย CDN: สิ่งที่รวมอยู่และการใช้งานอย่างเป็นธรรม

แพ็กเกจ WordPress แบบชำระเงินของ SmallPict มาพร้อมการส่งรูปภาพในเครื่องด้วยความเร็วสูง (Starter) หรือ Managed Global CDN ที่เลือกเปิดได้ (Pro และ Business) นี่คือสิ่งที่รวมอยู่อย่างชัดเจน สิ่งที่ถือเป็นการใช้งานอย่างเป็นธรรม และวิธีการทำงานของโควตาการใช้งานร่วมกันข้ามหลายเว็บไซต์