Lint, formatter, static analysis และ automated refactoring คืออะไร และเครื่องมือแต่ละตัวในโปรเจกต์ Laravel ทำหน้าที่ไหน

เรามักเรียก Pint, Rector, PHPStan, ESLint และ Prettier รวม ๆ ว่า lint ทั้งที่แต่ละตัวทำงานคนละแบบ โพสต์นี้แยกให้เห็นว่าเครื่องมือแต่ละตัวเป็นแบบไหน ดูอะไรในโค้ด แก้โค้ดให้หรือแค่รายงาน และรันตอนไหน

เผยแพร่
เวลาอ่าน
6 นาที
ในโพสต์นี้ 10 หัวข้อ
  1. เครื่องมือห้าตัวแบ่งได้สี่แบบ
  2. Formatter จัดหน้าตาโค้ดโดยไม่แตะความหมาย
  3. Linter หารูปแบบโค้ดที่น่าจะผิด
  4. Static analysis อ่านชนิดข้อมูลทั้งโปรเจกต์
  5. Automated refactoring เขียนโค้ดใหม่แทนเรา
  6. เครื่องมือที่แก้โค้ดได้จะมีโหมดตรวจอย่างเดียวด้วย
  7. ในเครื่องรัน formatter เป็นตัวสุดท้าย
  8. ให้ Claude Code จัดไฟล์ให้ทุกครั้งที่แก้เสร็จ
  9. ใน pull request ตรวจเฉพาะไฟล์ที่เปลี่ยน
  10. อ่านต่อทีละเครื่องมือ

เปิดโปรเจกต์ Laravel สักตัว เราจะเห็น pint.json, rector.php, phpstan.neon, eslint.config.js และ .prettierrc.json วางเรียงกันอยู่ที่ราก repo เวลาคุยกันในทีมก็มักเรียกรวม ๆ ว่า "รัน lint ก่อน push นะ" แม้แต่เอกสารของ Pint เองก็เรียกการรัน Pint ใน GitHub Actions ว่า linting

จริง ๆ ห้าไฟล์นี้เป็นของเครื่องมือสี่แบบ บางตัวจัดแค่ช่องว่างกับเครื่องหมายคำพูด บางตัวเขียนโค้ดใหม่ทั้งบรรทัด และบางตัวไม่แตะโค้ดเลย แค่รายงานว่าตรงไหนน่าจะพัง โพสต์นี้แยกให้ดูว่าแต่ละตัวทำงานแบบไหน และรันตอนไหนบ้าง ส่วนรายละเอียดของแต่ละตัวแยกไว้อีกสี่โพสต์ ลิงก์อยู่ในตาราง

เครื่องมือห้าตัวแบ่งได้สี่แบบ

เครื่องมือ แบบ ภาษา แก้โค้ดให้ไหม รันในเครื่อง ตรวจใน PR
Laravel Pint formatter PHP แก้ให้ vendor/bin/pint --dirty pint --test
Rector automated refactoring PHP แก้ให้ vendor/bin/rector process rector process --dry-run
PHPStan + Larastan static analysis PHP รายงานอย่างเดียว composer analyse รันทั้งโปรเจกต์ใน CI
ESLint linter JavaScript, JSX แก้ได้บางกฎ npm run lint:fix eslint
Prettier formatter JavaScript, JSX, CSS, JSON แก้ให้ npm run format prettier --check

คำสั่งในตารางมาจากโปรเจกต์ของเว็บนี้เอง composer analyse, npm run lint:fix และ npm run format เป็น script ที่ตั้งไว้ใน composer.json กับ package.json

Formatter จัดหน้าตาโค้ดโดยไม่แตะความหมาย

formatter ดูแค่หน้าตาของโค้ด เช่น ช่องว่างรอบเครื่องหมาย การย่อหน้า เครื่องหมายคำพูดเดี่ยวหรือคู่ บรรทัดว่างก่อน return และลำดับของ use ด้านบนไฟล์ โค้ดก่อนและหลังจัดทำงานเหมือนกันทุกอย่าง

// ก่อนรัน Pint
$slug=Str::slug( $title );
return $slug;

// หลังรัน Pint
$slug = Str::slug($title);

return $slug;

ประโยชน์ที่เห็นชัดที่สุดคือ diff ของ pull request สะอาดขึ้น ทุกคนในทีมเขียนโค้ดออกมาหน้าตาเดียวกัน diff จึงเหลือแค่บรรทัดที่เปลี่ยน logic จริง ไม่มีบรรทัดที่ editor ของใครสักคนจัด indent ใหม่ปนเข้ามา และไม่ต้องเถียงกันใน PR ว่าวงเล็บปีกกาควรขึ้นบรรทัดใหม่หรือเปล่า

ฝั่ง PHP ใช้ Laravel Pint ซึ่งติดมากับโปรเจกต์ Laravel ใหม่ทุกตัว ฝั่ง JavaScript ใช้ Prettier ที่ทำได้มากกว่าจัดช่องว่างอีกอย่าง คือเรียง class ของ Tailwind ให้ด้วย ถ้าติดตั้ง plugin ไว้

Linter หารูปแบบโค้ดที่น่าจะผิด

คำว่า lint มาจากโปรแกรมชื่อ lint บน Unix ที่ Stephen C. Johnson เขียนไว้ตรวจโค้ดภาษา C เมื่อปี 1978 ชื่อตั้งตามขุยผ้าที่ติดเสื้อ คือของเล็ก ๆ ที่ compiler ปล่อยผ่าน แต่ควรเก็บออก

linter อ่านโค้ดทีละไฟล์แล้วเทียบกับกฎที่ตั้งไว้ กฎแต่ละข้อดูรูปแบบที่มักเป็นบั๊ก เช่น ประกาศตัวแปรแล้วไม่ได้ใช้ หรือเรียก React hook ไว้ใน if ซึ่งทำให้ลำดับ hook เปลี่ยนระหว่าง render ผลที่ได้คือรายการคำเตือนพร้อมเลขบรรทัด บางกฎแก้ให้เองได้ถ้าสั่ง --fix แต่ส่วนใหญ่เราต้องอ่านแล้วแก้เอง

ในโปรเจกต์ Laravel ตัวที่ทำหน้าที่นี้คือ ESLint ฝั่ง JavaScript ฝั่ง PHP มี php -l ที่ตรวจได้แค่ว่า syntax ถูกไหม งานหาโค้ดที่น่าจะผิดเราจึงให้ static analysis ทำแทน

Static analysis อ่านชนิดข้อมูลทั้งโปรเจกต์

static analysis แปลตรงตัวว่าวิเคราะห์โค้ดตอนที่โค้ดยังไม่ได้รัน linter ก็นับเป็น static analysis แบบหนึ่ง แต่ในโลก PHP พอพูดคำนี้ เรามักหมายถึงเครื่องมืออย่าง PHPStan ที่ไล่ชนิดข้อมูลข้ามไฟล์ทั้งโปรเจกต์

PHPStan รู้ว่าเมธอดนี้คืนค่าอะไร ตัวแปรนี้เป็น object ของคลาสไหน และฟังก์ชันที่เราเรียกต้องการ argument ชนิดไหน จึงจับได้ว่าเราส่ง string เข้าไปในพารามิเตอร์ที่ต้องเป็น int หรือพิมพ์ชื่อ property ของ model ผิด แม้บรรทัดนั้นจะไม่มีเทสต์ไหนวิ่งผ่านเลยก็ตาม ส่วน Larastan เป็น extension ที่สอน PHPStan ให้รู้จัก Eloquent, facade และของอื่น ๆ ใน Laravel ที่ทำงานผ่าน magic method

PHPStan ไม่แก้โค้ดให้ ได้แค่รายงาน และเพราะต้องเห็นทั้งโปรเจกต์ถึงจะรู้ว่าใครเรียกใคร เราจึงรันทีละทั้งโปรเจกต์ ไม่ได้รันเฉพาะไฟล์ที่แก้

Automated refactoring เขียนโค้ดใหม่แทนเรา

refactoring คือปรับโครงสร้างโค้ดโดยที่ผลลัพธ์ยังเหมือนเดิม Rector ทำส่วนนี้ให้อัตโนมัติ โดยอ่านโค้ดเป็นโครงสร้าง แล้วเขียนใหม่ตามกฎที่เลือกไว้ กฎข้อหนึ่งดูแลการเปลี่ยนแบบเดียว เช่น ย้าย $casts จาก property ไปเป็นเมธอด casts() ตามแบบที่ Laravel รุ่นใหม่แนะนำ

// เดิม
protected $casts = [
    'published_at' => 'datetime',
];

// หลังรัน Rector
protected function casts(): array
{
    return [
        'published_at' => 'datetime',
    ];
}

Rector ต่างจาก formatter ตรงที่เปลี่ยนตัวโค้ดจริง และต่างจาก PHPStan ตรงที่แก้ให้เลยแทนการรายงาน งานที่ Rector ทำได้ดีคือการเปลี่ยนแบบเดียวกันซ้ำ ๆ ทั้งโปรเจกต์ เช่น เปลี่ยนมาใช้ syntax ของ PHP เวอร์ชันใหม่ หรือแก้โค้ดตอนอัปเกรด Laravel ข้ามเวอร์ชัน

เพราะ Rector เปลี่ยนโค้ดจริง ทุกครั้งที่รันควรอ่าน diff ก่อน commit บางกฎเปลี่ยนโค้ดได้ถูกตามหลัก แต่อ่านแล้วเข้าใจผิดได้ ซึ่งเล่าตัวอย่างไว้ในโพสต์ของ Rector

เครื่องมือที่แก้โค้ดได้จะมีโหมดตรวจอย่างเดียวด้วย

Pint, Rector, ESLint และ Prettier มีสองโหมดเหมือนกัน

  • โหมดแก้ ใช้ในเครื่องของเรา เครื่องมือเขียนทับไฟล์ให้เลย
  • โหมดตรวจ ใช้ใน pull request เครื่องมือไม่แตะไฟล์ แค่บอกว่าไฟล์ไหนยังไม่ผ่าน แล้วจบด้วย exit code ที่ไม่ใช่ 0 ให้ GitHub ขึ้นสถานะสีแดง ได้แก่ pint --test, rector process --dry-run, prettier --check และ eslint ที่ไม่ใส่ --fix

ทางนี้ทำให้ไม่มี bot ไปแก้โค้ดแล้ว commit ทับงานของใคร คนที่เปิด PR เห็นว่าไม่ผ่านตรงไหน ก็รันโหมดแก้ในเครื่องแล้ว push ใหม่

ในเครื่องรัน formatter เป็นตัวสุดท้าย

ลำดับมีผล Rector กับ eslint --fix เขียนโค้ดใหม่ได้ และโค้ดที่ได้อาจยังไม่ตรงกับรูปแบบที่ทีมใช้ เราจึงรัน formatter ตามหลังเสมอ ในโปรเจกต์นี้ใช้สองบรรทัดนี้ก่อน commit

vendor/bin/rector process && vendor/bin/pint --dirty
npm run lint:fix && npm run format

--dirty บอก Pint ให้จัดเฉพาะไฟล์ที่ยังไม่ได้ commit ส่วน PHPStan ไม่แก้โค้ด จะรัน composer analyse ก่อนหรือหลังก็ได้ ขอแค่รันก่อน push

ให้ Claude Code จัดไฟล์ให้ทุกครั้งที่แก้เสร็จ

ถ้าใช้ Claude Code ช่วยเขียนโค้ด เราจะเขียนไว้ใน CLAUDE.md ว่าแก้เสร็จแล้วให้รัน Pint ก็ได้ แต่ AI ทำตามคำสั่งพวกนี้ไม่ครบทุกครั้ง ทางที่แน่นอนกว่าคือ hook ซึ่งเป็นคำสั่ง shell ที่ Claude Code รันเองตามจังหวะที่ตั้งไว้ โดยไม่ต้องรอให้ Claude นึกขึ้นได้

จังหวะที่ใช้คือ PostToolUse ซึ่งรันหลัง Claude ใช้ tool แต่ละครั้ง และตั้ง matcher เป็น Edit|Write ให้รันเฉพาะหลังแก้หรือสร้างไฟล์

{
    "hooks": {
        "PostToolUse": [
            {
                "matcher": "Edit|Write",
                "hooks": [
                    {
                        "type": "command",
                        "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh"
                    }
                ]
            }
        ]
    }
}

Claude Code ส่งข้อมูลของ tool ที่เพิ่งรันมาเป็น JSON ทาง stdin ในนั้นมี path ของไฟล์ที่แก้อยู่ที่ tool_input.file_path script จึงดึง path ด้วย jq แล้วเลือกเครื่องมือตามชนิดไฟล์

#!/usr/bin/env bash
file=$(jq -r '.tool_input.file_path // empty')
[ -f "$file" ] || exit 0

cd "$CLAUDE_PROJECT_DIR" || exit 0

case "$file" in
  *.php)
    vendor/bin/pint --quiet "$file"
    ;;
  */resources/js/*.js | */resources/js/*.jsx)
    npx eslint --fix --max-warnings=0 "$file" >&2
    lint=$?
    npx prettier --write --log-level=warn "$file"
    # ESLint ยังเจอปัญหาที่แก้เองไม่ได้: exit 2 ให้ Claude เห็นข้อความแล้วแก้ต่อ
    [ "$lint" -eq 0 ] || exit 2
    ;;
esac
  • ไฟล์ PHP ให้ Pint จัดเฉพาะไฟล์นั้น เร็วพอที่จะรันทุกครั้งโดยไม่รู้สึก
  • ไฟล์ JavaScript ให้ ESLint แก้ส่วนที่แก้ได้ แล้ว Prettier จัดหน้าตาปิดท้าย --max-warnings=0 นับคำเตือนเป็นปัญหาด้วย
  • exit 2 บอก Claude Code ให้ส่งข้อความใน stderr กลับไปให้ Claude ถ้า ESLint ยังเจอปัญหาที่แก้เองไม่ได้ เช่น useEffect ที่ลืมใส่ dependency Claude จะเห็นคำเตือนนั้นแล้วแก้ต่อเอง ส่วนงานจัด formatting จบเงียบ ๆ ไม่ต้องรายงานใคร

อย่าลืม chmod +x .claude/hooks/format.sh และติดตั้ง jq ไว้ในเครื่อง ไฟล์ .claude/settings.json commit เข้า repo ได้ ทุกคนในทีมที่ใช้ Claude Code ก็จะได้ hook เดียวกัน ถ้าอยากใช้คนเดียวให้ใส่ไว้ใน .claude/settings.local.json แทน พิมพ์ /hooks ใน Claude Code ก็จะเห็นรายการ hook ที่ตั้งไว้ทั้งหมด

Rector กับ PHPStan ไม่ได้อยู่ใน hook นี้ Rector เปลี่ยนตัวโค้ดมากกว่ารูปแบบ เราอยากอ่าน diff ก่อนรับ ส่วน PHPStan ต้องอ่านทั้งโปรเจกต์ ถ้ารันหลังแก้ทุกไฟล์ก็ช้าเกินไป สองตัวนี้จึงยังรันก่อน commit เหมือนเดิม

ใน pull request ตรวจเฉพาะไฟล์ที่เปลี่ยน

เว็บนี้มี workflow ชื่อ pr-lint.yml รันทุกครั้งที่เปิด pull request เข้า main workflow หาไฟล์ PHP กับ JavaScript ที่ PR แก้ แล้วส่งเฉพาะไฟล์เหล่านั้นให้เครื่องมือแต่ละตัวในโหมดตรวจ

      - name: Pint
        run: vendor/bin/pint --test ${{ steps.changed.outputs.php }}

      - name: Rector
        run: vendor/bin/rector process --dry-run --no-progress-bar ${{ steps.changed.outputs.php }}

      - name: ESLint
        run: npx eslint ${{ steps.changed.outputs.js }}

      - name: Prettier
        run: npx prettier --check ${{ steps.changed.outputs.js }}

PHPStan ไม่ได้อยู่ใน workflow นี้ เพราะไฟล์ที่ไม่ได้แก้อาจพังจากไฟล์ที่แก้ก็ได้ เช่น เราเปลี่ยนชนิดที่เมธอดหนึ่งคืนค่า แล้วไฟล์อื่นที่เรียกเมธอดนั้นใช้ค่าผิดชนิด PHPStan จึงไปรันทั้งโปรเจกต์ใน CI หลักทุกครั้งที่มีการ push

อ่านต่อทีละเครื่องมือ

  • PHPStan และ Larastan ระดับความเข้มงวด 0 ถึง 10 เหตุผลที่ระดับ 5 เป็นจุดเริ่มที่ดี และ baseline สำหรับโปรเจกต์ที่มีโค้ดอยู่แล้ว
  • Rector ชุดกฎที่ควรเปิด การอัปเกรด Laravel ด้วย LaravelSetList และกฎที่เราเลือกข้าม
  • Laravel Pint preset laravel และกฎที่เปิดเพิ่มใน pint.json
  • ESLint และ Prettier สำหรับโปรเจกต์ Inertia กับ React ตั้งแต่ flat config ไปจนถึงการเรียง class ของ Tailwind

ตอนถัดไปของซีรีส์ เครื่องมือ Code Quality ใน Laravel

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →