CPX คืออะไร และวิธีรันคำสั่งของ Composer package โดยไม่ต้องลง global

CPX รันคำสั่งของ Composer package ได้จากทุกเครื่องแบบ npx โดยลงแต่ละ package แยกกันและอัปเดตให้เอง ถ้า CLI ของเราตอบเป็น JSON และบอกผลด้วย exit code ทั้งคนและ AI agent ก็เรียกใช้ได้ด้วยคำสั่งเดียว

เผยแพร่
เวลาอ่าน
6 นาที
สรุปจาก
Nico Devs · tighten.com
แท็ก
#AI#PHP#Tools
ในโพสต์นี้ 7 หัวข้อ
  1. ลง tool ด้วย composer global require มีปัญหาสองเรื่อง
  2. CPX คือ npx ของ Composer
  3. CLI ที่ agent ใช้ง่ายตอบเป็น JSON และบอกผลด้วย exit code
  4. สร้าง CLI ตรวจเลขบัตรประชาชนด้วย Symfony Console
  5. ลองรันผ่าน CPX ก่อนขึ้น Packagist
  6. บอก agent ว่ามีคำสั่งนี้ให้ใช้
  7. CPX เหมาะกับงานแบบไหน

ลง tool ด้วย composer global require มีปัญหาสองเรื่อง

ที่ผ่านมา tool ที่อยากใช้ได้จากทุกโปรเจกต์ เราก็มักลงด้วย composer global require อย่าง Laravel installer ที่ทำให้พิมพ์ laravel new ได้จากทุกที่ ก็ใช้ได้นะ แต่พอลงไว้หลายตัวจะเริ่มเจอสองเรื่อง

  • dependency ชนกัน global ของ Composer จริง ๆ คือโปรเจกต์เดียวที่มี composer.json กับ vendor ชุดเดียว ทุก tool ที่ลงไว้จึงต้องใช้ library เวอร์ชันเดียวกัน ถ้าตัวหนึ่งต้องการ symfony/console 6 อีกตัวต้องการ 7 Composer จะลงไม่ได้และขึ้นว่า Your requirements could not be resolved to an installable set of packages.
  • ไม่อัปเดตเอง tool จะค้างอยู่ที่เวอร์ชันที่ลงไว้ จนกว่าเราจะนึกได้แล้วรัน composer global update

ถ้ายังไม่คุ้นกับ composer.json, vendor และ version constraint ลองอ่านวิธีใช้ Composer จัดการ package ก่อนได้ครับ

CPX คือ npx ของ Composer

CPX ย่อมาจาก Composer Package Executor ทำหน้าที่เดียวกับ npx ของ npm คือรันคำสั่งของ package โดยไม่ต้องเพิ่ม package นั้นเข้าโปรเจกต์หรือเข้า global Liam Hammett เริ่มทำไว้ตั้งแต่ปี 2024 แล้วเดือนกรกฎาคม 2026 ทีม Laravel เขียนใหม่ทั้งหมดเป็น CPX 2.0 ย้ายมาอยู่ที่ laravel/cpx และ Taylor Otwell เปิดตัวใน keynote ของ Laracon US 2026 เวอร์ชันล่าสุดตอนนี้คือ 2.1.0 ใช้กับ PHP 8.3 ขึ้นไป

ลงครั้งเดียวด้วย Composer

composer global require cpx/cpx

ตัว CPX เองลงแบบ global ได้โดยไม่ชนใคร เพราะเป็นไฟล์ PHAR ที่รวม dependency ไว้ข้างในแล้ว แค่ต้องให้ global bin ของ Composer อยู่ใน PATH (ดู path ได้จาก composer global config bin-dir --absolute) จากนั้นก็รันคำสั่งของ package ไหนก็ได้

# สร้างโปรเจกต์ใหม่ โดยไม่ต้องลง laravel/installer ไว้ก่อน
cpx laravel/installer new shop

# ระบุเวอร์ชันด้วย constraint แบบเดียวกับใน composer.json
cpx nunomaduro/phpinsights:^2.0 analyse ./src

ครั้งแรกจะช้าสักไม่กี่วินาที เพราะ CPX ต้องโหลด package และ dependency ของมันไปลงแยกไว้ที่ ~/.cpx ครั้งต่อไปใช้ของเดิม และทุกครั้งที่รัน CPX จะเช็คเวอร์ชันใหม่ไม่เกินชั่วโมงละครั้ง ถ้ามีเวอร์ชันที่ตรง constraint ก็อัปเดตก่อนรัน ส่วน composer.json ของโปรเจกต์ไม่เปลี่ยนเลย

ถ้าโปรเจกต์ลง tool ตัวนั้นไว้ใน vendor/bin อยู่แล้ว CPX จะใช้ตัวในโปรเจกต์ก่อน เช่น cpx pint ก็คือ Pint เวอร์ชันที่โปรเจกต์ล็อกไว้ ถ้าอยากใช้ตัวที่ลงแยกจริง ๆ ใส่ --skip-local

คำสั่ง ทำอะไร
cpx alias laravel/installer laravel ตั้งชื่อสั้นให้ package จะได้ไม่ต้องพิมพ์ชื่อเต็มทุกครั้ง
cpx installed ดู package ที่เคยรันผ่าน CPX และใช้ล่าสุดเมื่อไหร่
cpx update อัปเดตทุกตัวตาม constraint ปกติไม่ต้องสั่ง เพราะเช็คให้เองอยู่แล้ว
cpx clean ลบ package ที่ไม่ได้ใช้มานาน ใส่ --all เพื่อลบทั้งหมด
cpx tinker เปิด REPL ถ้าอยู่ในโปรเจกต์ Laravel จะเรียก php artisan tinker ให้

CLI ที่ agent ใช้ง่ายตอบเป็น JSON และบอกผลด้วย exit code

AI agent อย่าง Claude Code หรือ Codex เรียก CLI ผ่าน shell แล้วอ่าน output กลับมา tool ที่พิมพ์ตารางสี ๆ หรือถามต่อว่า "ต้องการทำต่อไหม" คนอ่านง่าย แต่ agent ต้องเดาเอาเอง ต้นทางสรุป CLI ที่ agent ใช้ง่ายไว้สี่ข้อ

ข้อ เหตุผล
ผลลัพธ์เป็น JSON รูปเดิมทุกครั้ง agent กับ script อ่าน field ได้เลย ไม่ต้องตัดข้อความ
error ก็เป็น JSON ไม่มีกรอบ error สี ๆ ของ framework ปนมา
สำเร็จจบด้วย exit code 0 พลาดจบด้วย 1 ผู้เรียกรู้ผลได้โดยไม่ต้องอ่าน output
ไม่ถามอะไรกลับ agent ตอบคำถามแบบ interactive ไม่ได้ คำสั่งจะค้างรอ

CPX เองก็ทำตามนี้ ถ้ารู้ว่าไม่ได้รันใน terminal ของคน (agent เรียก ส่ง input ผ่าน pipe หรือใส่ --no-interaction) จะปิด progress bar กับคำถามทั้งหมด เหลือแค่ output ของ tool และ error ของ CPX เองก็เป็น JSON เช่นตอนพิมพ์ชื่อ package ผิด

{"success": false, "errors": ["The package \"acme/thai-idd\" could not be found."], "summary": {}}

สร้าง CLI ตรวจเลขบัตรประชาชนด้วย Symfony Console

ต้นทางทำ CLI ดูสภาพอากาศเป็นตัวอย่าง เราลองทำตัวที่ใกล้งานเรากว่า คือส่งเลขบัตรประชาชน 13 หลักเข้าไป แล้วได้ JSON กลับมาว่าหลักตรวจสอบตรงไหม (สูตรคำนวณอยู่ในโพสต์ ThaiIdCardRule) package มีแค่สามไฟล์ ใช้ Symfony Console ตัวเดียวกับที่ Artisan ใช้อยู่ข้างใต้

<?php

namespace Acme\ThaiId;

use InvalidArgumentException;

final class ThaiId
{
    /** @return array{id: string, valid: bool} */
    public static function check(string $id): array
    {
        $digits = preg_replace('/\D/', '', $id);

        if (strlen($digits) !== 13) {
            throw new InvalidArgumentException('ID must be 13 digits');
        }

        $sum = 0;
        for ($i = 0; $i < 12; $i++) {
            $sum += (int) $digits[$i] * (13 - $i);
        }

        return [
            'id' => $digits,
            'valid' => (11 - $sum % 11) % 10 === (int) $digits[12],
        ];
    }
}

ตรรกะอยู่ในคลาส ThaiId แยกจากไฟล์ใน bin ไฟล์ CLI จึงเหลือแค่รับ argument กับพิมพ์ผล และทดสอบตรรกะได้โดยไม่ต้องผ่าน CLI จุดที่ควรดูในไฟล์ bin/thai-id มีดังนี้

  • บรรทัด #!/usr/bin/env php ทำให้รันไฟล์ได้ตรง ๆ หลัง chmod +x bin/thai-id
  • require autoloader สองที่ ตอนพัฒนา autoloader อยู่ใน vendor/ ของ package เอง แต่พอ CPX หรือ Composer ลง package เป็น dependency ไฟล์นี้จะไปอยู่ใน vendor/acme/thai-id/bin autoloader จึงอยู่ถัดขึ้นไปสามชั้น ถ้าลืมข้อนี้ ทดสอบในเครื่องจะผ่าน แต่พังทันทีหลัง publish
  • setDefaultCommand('thai-id', true) ทำให้พิมพ์ thai-id <เลข> ได้โดยไม่ต้องใส่ชื่อ command
  • setCatchExceptions(false) คู่กับ try/catch ปกติ Symfony จะจับ exception แล้วพิมพ์กรอบแดงให้คนอ่าน เราปิดไว้แล้วจับเองที่เดียว ทั้ง exception ของเราและของ Symfony อย่างลืมใส่ argument หรือใส่ option ที่ไม่มี ทุกอย่างจึงออกมาเป็น {"error": …} และจบด้วย exit code 1

ส่วน bin ใน composer.json บอกทั้ง Composer และ CPX ว่าไฟล์ไหนคือคำสั่ง ชื่อไฟล์ตรงกับชื่อ package พอดี จึงพิมพ์แค่ cpx acme/thai-id <เลข> ได้โดยไม่ต้องบอกชื่อ binary ต้นทางเขียนด้วย pipe operator (|>) ของ PHP 8.5 ส่วนตัวอย่างนี้เขียนแบบธรรมดา จึงใช้กับ PHP 8.4 ที่ Symfony Console 8 ต้องการได้

ลองรันดูหลัง composer install

bin/thai-id 1234567890121
{"id":"1234567890121","valid":true}
bin/thai-id 12345
{"error":"ID must be 13 digits"}
bin/thai-id
{"error":"Not enough arguments (missing: \"id\")."}

สังเกตว่าเลขที่หลักตรวจสอบไม่ตรงไม่นับเป็น error คำสั่งทำงานสำเร็จ แค่ผลออกมาเป็น "valid": false และจบด้วย exit code 0 แยกผลลัพธ์ออกจาก error ให้ชัด agent จะรู้ว่าควรแจ้งผู้ใช้ หรือควรแก้คำสั่งแล้วลองใหม่

ลองรันผ่าน CPX ก่อนขึ้น Packagist

ก่อนออกเวอร์ชันแรก ลองรัน cpx . 1234567890121 ในโฟลเดอร์ของ package ดูก่อน CPX จะอ่าน bin จาก composer.json แล้วรันเหมือนตอน publish แล้ว ถ้า metadata ผิดจะเห็นตั้งแต่ตอนนี้ (ต้องรัน composer install ไว้ก่อน เพราะ CPX ไม่ลง dependency ให้ package ในเครื่อง)

ทีนี้ก็ publish ได้ push ขึ้น GitHub ติด tag v0.1.0 แล้วส่ง repo ที่ packagist.org/packages/submit ถ้าเปิด GitHub integration ของ Packagist ไว้ tag ใหม่จะขึ้นเองทุกครั้ง ต้นทางทำ GitHub Action ที่เพิ่มเลข patch แล้วติด tag ให้ทุกครั้งที่ push เข้า main ด้วย เพราะ Packagist ดูที่ tag การติด tag จึงเท่ากับออกเวอร์ชันใหม่

หลังจากนั้นใครก็รัน cpx acme/thai-id 1234567890121 ได้จากทุกเครื่องที่มี PHP กับ CPX

บอก agent ว่ามีคำสั่งนี้ให้ใช้

agent ไม่รู้เองว่ามีคำสั่งนี้ เขียนไว้ใน AGENTS.md, CLAUDE.md หรือ skill ว่าเรียกยังไง ผลเป็นแบบไหน และพลาดแล้วควรทำอะไรต่อ

ตรวจเลขบัตรประชาชนด้วย `cpx acme/thai-id <เลข 13 หลัก>`
ผลเป็น JSON ที่มี `valid` บอกว่าหลักตรวจสอบตรงหรือไม่
ถ้าได้ `error` ให้เช็คว่าส่งเลขครบ 13 หลักหรือยัง อย่าเดาเลขใหม่เอง

ฝั่ง agent มีแค่ PHP กับ CPX ก็พอ ไม่ต้องมี SDK หรือ server ถ้าโปรเจกต์ใช้ Laravel Boost อยู่แล้ว ลองดูวิธีจัดไฟล์ให้ agent อ่านในโพสต์ Laravel Boost กับ AGENTS.md ได้ครับ

CPX เหมาะกับงานแบบไหน

  • installer และ tool ที่ใช้นาน ๆ ครั้ง อย่าง laravel/installer หรือ PHP Insights ใช้แทน composer global require ได้ตรง ๆ
  • tool ของทีมเอง เขียนครั้งเดียว ทุกคนและ agent ในทุกเครื่องรันได้ด้วยชื่อ package แก้แล้วติด tag ใหม่ ทุกเครื่องก็ได้เวอร์ชันใหม่ภายในชั่วโมง
  • tool ที่ผูกกับโปรเจกต์ อย่าง Pint หรือ PHPUnit ยังลงไว้ใน require-dev เหมือนเดิม เวอร์ชันจะล็อกอยู่ใน composer.lock และ CPX ก็ใช้ตัวในโปรเจกต์อยู่แล้ว

อัปเดตเองก็มีอีกด้าน package ที่อัปเดตเองทุกชั่วโมงจะได้เวอร์ชันใหม่ทันทีที่ออก รวมถึงเวอร์ชันปลอมจากบัญชีที่โดนยึดด้วย (ความเสี่ยงเดียวกับที่ทำให้เราตั้ง cooldown ใน Composer) tool ที่มีสิทธิ์เยอะ ระบุเวอร์ชันให้แคบ เช่น cpx vendor/package:2.1.0 จะอุ่นใจกว่า

ต้นทางเองก็บอกว่าตัวอย่างดูสภาพอากาศไม่ได้มีประโยชน์ในตัวเอง เพราะ agent เรียก API ตรง ๆ ก็ได้ สิ่งที่ได้คือโครง package ที่เอาไปใช้ซ้ำกับ tool อื่นได้ ถ้าอยากเห็นโค้ดเต็มรวมถึงไฟล์ GitHub Action ลองอ่านต่อที่บทความของ Tighten และคำสั่งทั้งหมดของ CPX อยู่ใน README ของ laravel/cpx

อ่านต่อ

ล่าสุด

ดูทั้งหมด →