Rector ปรับโค้ดและอัปเกรดเวอร์ชัน PHP กับ Laravel ให้อัตโนมัติ

Rector อ่านโค้ด PHP แล้วเขียนใหม่ตามกฎที่เลือกไว้ ตั้งแต่อัปเกรดเวอร์ชัน PHP และ Laravel ไปจนถึงลบ dd() ที่หลงเหลือ โพสต์นี้อธิบาย rector.php ของโปรเจกต์ Laravel ชุดกฎที่ควรเปิด และกฎที่เราเลือกข้ามเพราะทำให้โค้ดอ่านแล้วเข้าใจผิด

เผยแพร่
เวลาอ่าน
6 นาที
ในโพสต์นี้ 7 หัวข้อ
  1. Rector อ่านโค้ดเป็นโครงสร้าง แล้วเขียนใหม่ตามกฎ
  2. rector.php ของเว็บนี้
  3. อัปเกรด PHP ด้วย withPhpSets()
  4. rector-laravel มีชุดอัปเกรดของ Laravel แต่ละเวอร์ชัน
  5. เช็ก dd() ที่หลงเหลือด้วย RemoveDumpDataDeadCodeRector
  6. กฎที่เราเลือกข้าม
  7. รัน Rector ก่อน Pint

อัปเกรด Laravel แต่ละครั้ง ที่ผ่านมาเราก็มักเปิด upgrade guide ไว้ข้าง ๆ แล้วไล่แก้ทีละไฟล์ เปลี่ยนชื่อเมธอดที่เลิกใช้ ย้าย config ไปที่ใหม่ แก้ทีละจุดจนครบ งานพวกนี้ส่วนใหญ่เป็นการเปลี่ยนแบบเดิมซ้ำ ๆ หลายสิบที่ ซึ่งเป็นงานที่ Rector ทำแทนเราได้

Rector เป็นเครื่องมือ automated refactoring หนึ่งในสี่แบบของเครื่องมือตรวจโค้ด ถ้ายังไม่แน่ใจว่าต่างจาก Pint หรือ PHPStan ยังไง ลองอ่านโพสต์ที่แยกเครื่องมือแต่ละแบบก่อนได้ครับ

Rector อ่านโค้ดเป็นโครงสร้าง แล้วเขียนใหม่ตามกฎ

ถ้าใช้ find and replace เปลี่ยนโค้ด เราเห็นโค้ดเป็นแค่ข้อความ แยกไม่ออกว่า where ตรงนี้เป็นเมธอดของ query builder หรือเป็นคำในคอมเมนต์ Rector อ่านโค้ดเป็นโครงสร้าง (AST) รู้ว่าตรงไหนเป็นคลาส เมธอด argument หรือเงื่อนไข และใช้ PHPStan ช่วยดูว่าตัวแปรแต่ละตัวเป็นชนิดอะไร จึงเปลี่ยนเฉพาะจุดที่ตรงกับกฎจริง ๆ

กฎ (rule) ข้อหนึ่งดูแลการเปลี่ยนแบบเดียว เช่น เปลี่ยน $casts จาก property เป็นเมธอด casts() กฎหลายข้อรวมกันเป็นชุด (set) เช่น ชุดของ PHP แต่ละเวอร์ชัน หรือชุด dead code ที่ลบโค้ดที่ไม่มีวันทำงาน

ติดตั้ง Rector คู่กับ rector-laravel ที่เพิ่มกฎของ Laravel

composer require --dev rector/rector driftingly/rector-laravel

รัน vendor/bin/rector ครั้งแรก ถ้ายังไม่มี rector.php Rector จะถามว่าจะให้สร้างไฟล์ตั้งต้นให้ไหม จากนั้นสั่ง --dry-run เพื่อดูว่าจะเปลี่ยนอะไรบ้าง โดยยังไม่แตะไฟล์ พอดู diff แล้วโอเค ค่อยรันโดยไม่ใส่ --dry-run

vendor/bin/rector process --dry-run
vendor/bin/rector process

เอกสารของ Rector แนะนำให้โปรเจกต์ที่มีโค้ดอยู่แล้วเริ่มจากชุดเล็ก ๆ เช่น withTypeCoverageLevel(0) แล้วขยับทีละระดับ ทุกระดับเปิดกฎเพิ่มทีละข้อ diff จึงเล็กพอให้อ่านจบใน PR เดียว

rector.php ของเว็บนี้

return RectorConfig::configure()
    ->withPaths([__DIR__.'/app', __DIR__.'/bootstrap', __DIR__.'/config', __DIR__.'/database', __DIR__.'/routes', __DIR__.'/tests'])
    ->withPhpSets(php84: true)
    ->withPreparedSets(deadCode: true, codeQuality: true, earlyReturn: true, namedArgs: true, carbon: true)
    ->withSets([
        LaravelSetList::LARAVEL_130_WITHOUT_ATTRIBUTES,
        LaravelSetList::LARAVEL_120,
        LaravelSetList::LARAVEL_CODE_QUALITY,
        LaravelSetList::LARAVEL_COLLECTION,
        LaravelSetList::LARAVEL_TYPE_DECLARATIONS,
        LaravelSetList::LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER,
    ])
    ->withRules([
        MigrateToSimplifiedAttributeRector::class,
        ModelCastsPropertyToCastsMethodRector::class,
    ])
    ->withTypeCoverageLevel(0)
    ->withSkip([
        __DIR__.'/bootstrap/cache',
        __DIR__.'/bootstrap/ssr',
        AppToResolveRector::class,
        ApplyDefaultInsteadOfNullCoalesceRector::class,
        AddNameToBooleanArgumentRector::class,
        RemoveDefaultValueFromAssignedPropertyRector::class,
        ServerVariableToRequestFacadeRector::class => [__DIR__.'/bootstrap'],
    ]);

ไล่ทีละส่วน

  • withPaths() บอกว่าให้ Rector ดูโฟลเดอร์ไหน
  • withPhpSets(php84: true) เปลี่ยนโค้ดมาใช้ syntax ที่ PHP มีจนถึง 8.4 เช่น constructor property promotion หรือ match แทน switch ที่คืนค่าอย่างเดียว (ดูของใหม่ใน PHP 8.4 ได้ที่โพสต์นี้)
  • withPreparedSets() เปิดชุดที่ Rector เตรียมไว้ deadCode ลบโค้ดที่ไม่มีวันทำงาน codeQuality ทำโค้ดให้สั้นและตรงขึ้น earlyReturn เปลี่ยน if ซ้อนกันเป็น return ก่อน namedArgs ใส่ชื่อให้ argument บางแบบ และ carbon ปรับวิธีใช้ Carbon
  • withSets() เปิดชุดของ rector-laravel ซึ่งอธิบายในหัวข้อถัดไป
  • withRules() เปิดกฎทีละข้อที่ไม่ได้อยู่ในชุดไหน เช่น ย้าย $casts ไปเป็นเมธอด casts()
  • withSkip() ข้ามโฟลเดอร์ หรือข้ามกฎที่ไม่อยากได้ จะข้ามทั้งโปรเจกต์ หรือข้ามแค่บางโฟลเดอร์ก็ได้ อย่างบรรทัดสุดท้ายที่ข้าม ServerVariableToRequestFacadeRector เฉพาะใน bootstrap

อัปเกรด PHP ด้วย withPhpSets()

Rector มีชุดกฎของ PHP ทุกเวอร์ชัน ตั้งแต่ 5.3 จนถึง 8.6 ถ้าเรียก withPhpSets() โดยไม่ใส่ argument Rector จะอ่านเวอร์ชันขั้นต่ำจาก require.php ใน composer.json แล้วเปิดกฎทุกชุดจนถึงเวอร์ชันนั้นให้ Rector เองก็แนะนำให้เรียกแบบนี้ จะได้ไม่ต้องกลับมาแก้ rector.php ทุกครั้งที่ขยับเวอร์ชัน

return RectorConfig::configure()
    ->withPhpSets();

ขั้นตอนอัปเกรดจึงเหลือแค่แก้ "php": "^8.4" ใน composer.json แล้วรัน rector process --dry-run ดู diff งานที่ Rector ทำแบ่งได้สองแบบ

  • เปลี่ยนมาใช้ syntax ใหม่ เช่น constructor property promotion หรือ match แทน switch
  • แก้สิ่งที่ PHP เวอร์ชันใหม่ประกาศว่าเลิกใช้ (deprecated) อย่าง PHP 8.4 ที่เลิกให้เขียนพารามิเตอร์ที่มีค่าเริ่มต้นเป็น null โดยไม่ใส่ ? ไว้หน้า type
// เดิม: PHP 8.4 ขึ้นคำเตือน deprecated
public function publish(Post $post, CarbonImmutable $at = null): void

// หลังรัน Rector
public function publish(Post $post, ?CarbonImmutable $at = null): void

ถ้าโปรเจกต์ค้างอยู่ที่เวอร์ชันเก่ามาก แล้วเปิดทีเดียวได้ diff เป็นพันไฟล์ ให้ใช้ withPhpLevel() แทน ซึ่งเปิดกฎทีละข้อแบบเดียวกับ withTypeCoverageLevel()

Rector แก้ได้เฉพาะสิ่งที่เห็นจากตัวโค้ด ส่วนพฤติกรรมที่เปลี่ยนตอนรัน เช่น ฟังก์ชันที่คืนค่าต่างจากเดิม หรือ extension ที่ต้องติดตั้งเพิ่มบนเซิร์ฟเวอร์ ยังต้องอ่านจาก migration guide ของ PHP และรันเทสต์บน PHP เวอร์ชันใหม่อยู่ดี

rector-laravel มีชุดอัปเกรดของ Laravel แต่ละเวอร์ชัน

rector-laravel มีชุดกฎสองกลุ่ม

กลุ่มแรกเป็นชุดอัปเกรด ชุดละหนึ่งขั้น LARAVEL_120 คือจาก 11 ไป 12 และ LARAVEL_130 คือจาก 12 ไป 13 ตอนอัปเกรด Laravel เราเปิดชุดของเวอร์ชันที่จะไป รัน --dry-run อ่าน diff แล้วค่อยรันจริง งานที่เหลือให้ทำเองคือเรื่องที่ Rector ตัดสินแทนไม่ได้ เช่น พฤติกรรมที่เปลี่ยนไป ซึ่งยังต้องอ่านจาก upgrade guide

ชุด Laravel 13 แบ่งเป็นสองแบบ แบบเต็มจะเปลี่ยน property ของคลาสอย่าง $table, $fillable หรือ $tries ให้เป็น PHP attribute ตามที่ Laravel 13 รองรับ เว็บนี้ยังอยากเก็บไว้เป็น property จึงเลือก LARAVEL_130_WITHOUT_ATTRIBUTES ที่ข้ามกฎกลุ่มนั้น

README ของ rector-laravel ตอนนี้แนะนำให้ใช้ withComposerBased() แทนการไล่ใส่ชุดอัปเกรดเอง Rector จะอ่านเวอร์ชัน Laravel จาก composer.json แล้วเลือกชุดอัปเกรดจนถึงเวอร์ชันที่ติดตั้งอยู่ให้ ส่วนชุดอัปเกรดแบบใส่เองอย่าง LaravelSetList::LARAVEL_130 ตอนนี้ขึ้นว่า deprecated แล้ว และจะหายไปในเวอร์ชันหลักถัดไป

return RectorConfig::configure()
    ->withComposerBased(laravel: true);

กลุ่มที่สองเป็นชุดที่ไม่ผูกกับเวอร์ชัน ตัวที่เว็บนี้เปิดไว้คือ

  • LARAVEL_CODE_QUALITY ปรับโค้ดให้ใช้ของที่ Laravel มีให้แล้ว
  • LARAVEL_COLLECTION ปรับการใช้ collection
  • LARAVEL_TYPE_DECLARATIONS เติม type ให้โค้ด Laravel ที่ Rector รู้ชนิดได้แน่นอน
  • LARAVEL_ELOQUENT_MAGIC_METHOD_TO_QUERY_BUILDER เปลี่ยนการเรียกแบบ Post::find(1) เป็น Post::query()->find(1) ให้ editor กับ PHPStan รู้ว่ากำลังทำงานกับ query builder

เช็ก dd() ที่หลงเหลือด้วย RemoveDumpDataDeadCodeRector

dd() กับ dump() ที่ใส่ไว้ตอนไล่บั๊กแล้วลืมลบ ถ้าหลุดขึ้น production หน้านั้นจะหยุดทำงาน แล้วแสดงข้อมูลข้างในให้ผู้ใช้เห็นทันที rector-laravel มีกฎ RemoveDumpDataDeadCodeRector ที่ลบการเรียกฟังก์ชันพวกนี้ออก กฎนี้ไม่ได้อยู่ในชุดไหน ต้องเปิดเอง และเลือกได้ว่าจะให้หาฟังก์ชันอะไรบ้าง

use RectorLaravel\Rector\FuncCall\RemoveDumpDataDeadCodeRector;

return RectorConfig::configure()
    ->withConfiguredRule(RemoveDumpDataDeadCodeRector::class, ['dd', 'dump', 'ray']);
// เดิม
public function publish(Post $post): void
{
    dd($post);

    $post->update(['published_at' => now()]);
}

// หลังรัน Rector
public function publish(Post $post): void
{
    $post->update(['published_at' => now()]);
}

ในเครื่อง กฎนี้ลบ dd() ให้เลย ส่วนใน pull request ที่รัน rector process --dry-run ไฟล์ที่ยังมี dd() ค้างอยู่จะทำให้ workflow ไม่ผ่าน กฎข้อเดียวจึงเป็นได้ทั้งตัวลบและตัวตรวจ

กฎนี้ดูเฉพาะการเรียกแบบฟังก์ชัน ส่วน ->dd() หรือ ->dump() ที่ต่อท้าย query builder หรือ collection กฎนี้ไม่เห็น ถ้าอยากได้อีกชั้น Pest มี arch test ที่เขียนว่า expect(['dd', 'dump'])->not->toBeUsed() ได้ เทสต์จะไม่ผ่านถ้ามีไฟล์ไหนเรียกใช้

กฎที่เราเลือกข้าม

กฎทุกข้อของ Rector เขียนไว้ให้ใช้กับโค้ด PHP ทั่วไป บางข้อจึงเข้ากับโค้ด Laravel ได้ไม่ดี ตัวอย่างที่เห็นชัดคือ AddNameToBooleanArgumentRector ในชุด namedArgs กฎนี้ใส่ชื่อพารามิเตอร์ให้ argument ที่เป็น true หรือ false เพราะ true เปล่า ๆ ไม่บอกว่าหมายถึงอะไร

กับฟังก์ชันของ PHP กฎนี้ช่วยได้จริง

// เดิม
in_array($tag, $tags, true);

// หลังรัน Rector
in_array($tag, $tags, strict: true);

แต่พอเจอ where() ของ Eloquent ผลออกมาแบบนี้ (diff จริงจาก --dry-run)

// เดิม
Post::query()->where('is_active', true)->get();

// หลังรัน Rector
Post::query()->where('is_active', operator: true)->get();

where() ประกาศพารามิเตอร์ไว้เป็น where($column, $operator = null, $value = null, $boolean = 'and') argument ตัวที่สองจึงชื่อ $operator ตามหลักแล้ว Rector ใส่ชื่อถูก แต่ตอนเรียก where() แบบสอง argument Laravel เอาตัวที่สองไปใช้เป็นค่าที่จะเทียบ และใช้ = เป็น operator ให้เอง operator: true จึงอ่านแล้วเข้าใจผิด คนที่มาอ่านทีหลังอาจคิดว่า true เป็นเครื่องหมายเปรียบเทียบอะไรสักอย่าง

กฎแบบนี้ข้ามได้ด้วยการใส่ชื่อคลาสไว้ใน withSkip() โดยยังเปิดชุด namedArgs ไว้ได้ เว็บนี้ข้ามไว้อีกสามข้อด้วยเหตุผลคล้ายกัน คือเปลี่ยนแล้วโค้ดไม่ได้อ่านง่ายขึ้นสำหรับทีมเรา ทุกครั้งที่เปิดชุดใหม่ ให้อ่าน diff จาก --dry-run ทั้งหมดก่อน เจอกฎที่ไม่ชอบก็ข้ามทีละข้อ

รัน Rector ก่อน Pint

โค้ดที่ Rector เขียนให้อาจยังไม่ตรงกับรูปแบบที่ทีมใช้ เช่น ช่องว่างหรือการขึ้นบรรทัด เราจึงรัน Laravel Pint ตามหลังทุกครั้ง

vendor/bin/rector process && vendor/bin/pint --dirty

ใน pull request เว็บนี้รัน rector process --dry-run เฉพาะไฟล์ที่ PR แก้ ถ้ายังมีจุดที่ Rector จะเปลี่ยน workflow จะไม่ผ่าน คนเปิด PR ก็รัน Rector ในเครื่องแล้ว push ใหม่

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

อ่านต่อ

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

ล่าสุด

ดูทั้งหมด →