pint --test và Static analysis
Hai job này hay bị gộp chung một chỗ trong pipeline và gọi luôn là "lint", nhưng chúng bắt hai loại lỗi khác hẳn nhau. Trả lời phỏng vấn mà gộp làm một là mất điểm ngay. Tài liệu này tách rõ: mỗi cái là gì, bắt được gì, thiếu thì mất gì thật.
Trả lời ngắn:
pint --testchỉ kiểm tra hình thức code — thiếu nó thì không có bug nào lọt production, chỉ tốn thời gian review và diff bẩn. Static analysis (PHPStan/Larastan) kiểm tra ngữ nghĩa và kiểu dữ liệu — thiếu nó thì bug thật sự lọt ra, ở đúng những nhánh code mà test không phủ tới. Nếu chỉ được thêm một job vào CI: chọn static analysis.
Từ điển nhanh
| Thuật ngữ | Nghĩa ngắn gọn |
|---|---|
| linter | Công cụ đọc code và báo chỗ sai quy ước — không chạy code |
| formatter / code style fixer | Công cụ sửa hình thức code cho đồng nhất (khoảng trắng, thứ tự import, dấu ngoặc) |
| static analysis (phân tích tĩnh) | Đọc code như một compiler và suy ra kiểu dữ liệu, tìm lỗi mà không chạy code |
| dry run | Chạy thử: làm hết mọi việc trừ việc ghi file, chỉ báo kết quả |
| exit code | Số lệnh trả về khi kết thúc; 0 = ok, khác 0 = lỗi. CI dựa vào đây để đỏ/xanh |
| preset (Pint) | Bộ quy tắc định sẵn: laravel (mặc định), psr12, symfony, empty |
| rule level (PHPStan) | Mức nghiêm khắc 0→10, càng cao càng bắt nhiều, cộng dồn từ mức dưới |
| baseline | File ghi lại toàn bộ lỗi đang có để bỏ qua, chỉ bắt lỗi mới sinh ra |
| risky fixer | Quy tắc sửa code có thể đổi hành vi chứ không chỉ đổi hình thức |
| false positive | Công cụ báo lỗi nhưng thật ra code đúng |
| Larastan | Extension dạy PHPStan hiểu "phép thuật" của Laravel (facade, Eloquent, container) |
| result cache | PHPStan nhớ kết quả lần trước, lần sau chỉ phân tích file đã đổi |
1. Nó là gì / chạy thế nào
1.1. Ba tấm lưới bắt ba loại cá khác nhau
Trước khi đi vào chi tiết, cần thấy bức tranh chung. Một pipeline có ba tầng kiểm tra, mỗi tầng bắt loại lỗi mà tầng kia không thể bắt được:
flowchart TD CODE["Code vừa viết"] --> F["Formatter
pint --test"] CODE --> S["Static analysis
phpstan analyse"] CODE --> T["Test
phpunit / pest"] F --> F1["Hình thức:
thụt lề, thứ tự import,
khoảng trắng, trailing comma"] S --> S1["Ngữ nghĩa:
gọi method không tồn tại,
truyền sai kiểu, null không kiểm tra"] T --> T1["Hành vi:
logic nghiệp vụ đúng chưa,
tích hợp DB/queue chạy được không"] F1 --> R["Merge được"] S1 --> R T1 --> R
| Chạy code? | Phủ được bao nhiêu | Bắt lỗi gì | Tốc độ | |
|---|---|---|---|---|
| Pint | Không | 100% file | Chỉ hình thức | Vài giây |
| PHPStan | Không | 100% dòng code | Kiểu dữ liệu, code chết, gọi sai | Chục giây → vài phút |
| Test | Có | Chỉ nhánh nào có test | Logic sai, tích hợp hỏng | Phút → chục phút |
Điểm mấu chốt để trả lời phỏng vấn: static analysis phủ 100% dòng code mà không cần viết một dòng test nào. Test phủ sâu nhưng chỉ ở nơi bạn nhớ viết test. Đó là lý do hai thứ này bổ sung nhau chứ không thay thế nhau.
1.2. Pint là gì — và --test làm gì
Laravel Pint là code style fixer cho PHP, bọc bên ngoài
PHP-CS-Fixer với cấu hình sẵn theo phong cách Laravel. Laravel 9+ ship kèm luôn
trong composer.json, không cần cài thêm.
Chạy trần thì nó ghi đè file:
# Sửa thẳng vào file — dùng ở máy dev
vendor/bin/pint
# Chỉ file đang sửa dở (theo git status) — nhanh hơn nhiều
vendor/bin/pint --dirty
# Chỉ file khác so với branch chỉ định
vendor/bin/pint --diff main
Trên CI thì không được ghi đè file — pipeline sửa code rồi vứt đi thì vô nghĩa,
mà commit ngược lại thì sinh cả đống rắc rối (xem phần 4). Nên có --test:
# Dry run: chỉ báo file nào sai, KHÔNG sửa gì
vendor/bin/pint --test
Cơ chế bên trong đúng như tên gọi — Pint vẫn tính ra bản sửa, nhưng thay vì ghi file thì nó so sánh và trả exit code:
flowchart LR A["Đọc file .php"] --> B["Áp bộ rule
của preset"] B --> C{"Bản sau khác
bản gốc?"} C -- "Không" --> D["exit 0
CI xanh"] C -- "Có" --> E{"--test?"} E -- "Có" --> F["In danh sách file sai
exit 1 — CI đỏ"] E -- "Không" --> G["Ghi đè file
exit 0"]
Logic exit code thật trong source Pint (rút gọn) — đáng nhớ vì nó giải thích cả
--repair:
$failure = (
// dry run (--test) hoặc --repair mà có thay đổi
($summary->isDryRun() || $this->input->getOption('repair')) && count($changes) > 0
)
|| count($this->errors->getInvalidErrors()) > 0 // file parse không được
|| count($this->errors->getExceptionErrors()) > 0
|| count($this->errors->getLintErrors()) > 0;
return $failure ? Command::FAILURE : Command::SUCCESS;
Chú ý dòng
getInvalidErrors(): Pint còn đỏ khi file không parse được — tức là syntax error. Đây là lợi ích phụ ít người để ý:pint --testchạy vài giây đã bắt được lỗi cú pháp trước khi test chạy vài phút mới chết.
Các flag CLI đáng biết:
| Flag | Làm gì | Dùng khi nào |
|---|---|---|
--test | Dry run, không sửa file, exit 1 nếu có sai | CI |
--repair | Sửa file và exit 1 nếu đã sửa gì | Pre-commit hook (sửa xong chặn commit để bạn xem lại) |
--bail | Dừng ngay ở lỗi đầu tiên trong dry run | Repo lớn, muốn CI đỏ nhanh |
--dirty | Chỉ file thay đổi theo git status | Máy dev |
--diff main | Chỉ file khác so với branch main | CI trên MR — nhanh hơn quét cả repo |
--format=json | Xuất JSON | Đẩy kết quả sang tool khác / annotate MR |
--parallel, --max-processes | Chạy nhiều tiến trình | Repo lớn |
--cache-file | Trỏ file cache | CI có cache giữa các lần chạy |
-v | In ra rule nào đã sửa từng file | Debug khi bất đồng với Pint |
Cấu hình bằng pint.json ở gốc repo:
{
"preset": "laravel",
"rules": {
"declare_strict_types": true,
"no_unused_imports": true,
"ordered_imports": { "sort_algorithm": "alpha" }
},
"exclude": ["database/migrations/0001_01_01_000000_baseline.php"]
}
1.3. Static analysis là gì — chạy thế nào bên dưới
Static analysis = đọc code và suy luận, không chạy code. PHP là ngôn ngữ động, lỗi kiểu chỉ nổ lúc runtime; static analysis mô phỏng lại việc "chạy" ở mức kiểu dữ liệu để tìm ra chỗ chắc chắn sẽ nổ.
Cơ chế của PHPStan qua 4 bước:
flowchart TD A["Parse code → AST
(cây cú pháp)"] --> B["Đọc PHPDoc + type hint
+ extension (Larastan)
→ dựng bảng kiểu"] B --> C["Duyệt từng nhánh code,
suy ra kiểu của mọi biểu thức
tại mọi điểm"] C --> D["Áp rule theo level
→ báo lỗi"] D --> E{"Có lỗi?"} E -- "Có" --> F["exit 1"] E -- "Không" --> G["exit 0"]
Ví dụ cụ thể nhất để hiểu "suy ra kiểu":
function getUser(int $id): ?User
{
return User::find($id);
}
// PHPStan biết getUser() trả về User|null.
// Sau dòng dưới, kiểu của $user là User|null.
$user = getUser(5);
// → Lỗi: "Cannot access property $name on User|null"
echo $user->name;
// Sau if, PHPStan "thu hẹp" (narrow) kiểu xuống còn User → hết lỗi.
if ($user !== null) {
echo $user->name;
}
Không có test nào cần viết. Không cần biết dòng đó có được gọi hay không.
Bug Attempt to read property "name" on null — lỗi phổ biến bậc nhất trong Laravel —
bị chặn ngay ở CI.
1.4. Rule level 0 → 10
PHPStan có 11 mức, cộng dồn: mức cao bao gồm mọi check của mức thấp. Ý đồ thiết kế là để repo cũ áp dụng dần, không phải sửa 5000 lỗi trong một ngày.
| Level | Bắt thêm gì | Ghi chú thực tế |
|---|---|---|
| 0 | Class/function/method không tồn tại; biến chưa định nghĩa | Gần như repo nào cũng qua được |
| 1 | Biến có thể chưa định nghĩa; magic method/property lạ | Bắt được bug "biến chỉ gán trong nhánh if" |
| 2 | Method lạ trên mọi biểu thức; kiểm tra PHPDoc có hợp lệ | Bắt PHPDoc viết sai/lỗi thời |
| 3 | Kiểu của giá trị return và gán vào property | Bắt đầu thấy giá trị thật |
| 4 | Dead code cơ bản: instanceof luôn false, code không bao giờ chạy tới | Rất hay lộ ra logic thừa sau refactor |
| 5 | Kiểu của tham số truyền vào hàm/method | Mốc thực tế tốt cho dự án Laravel |
| 6 | Bắt buộc phải khai báo kiểu (thiếu type hint = lỗi) | Repo cũ sẽ nổ hàng nghìn lỗi ở đây |
| 7 | Union type dùng sai — từ đây "có thể đúng" bị coi là lỗi | Ranh giới logic: ≤6 chấp nhận maybe, ≥7 đòi yes |
| 8 | Gọi method trên giá trị có thể null | Bắt đúng loại bug hay gặp nhất; đắt để đạt được |
| 9 | Siết mixed | Cần code base có type đầy đủ |
| 10 | Siết cả mixed ngầm định từ thư viện ngoài / baseline | Dự án greenfield kỷ luật cao mới nên nhắm |
Với Laravel bắt buộc cài
larastan/larastan. Không có nó, PHPStan không hiểuUser::where(...)(static call vào method không tồn tại), không hiểu facadeCache::get(), không hiểu$model->relation. Chạy PHPStan trần trên Laravel = hàng nghìn false positive, cả team bỏ cuộc sau một ngày.
File phpstan.neon điển hình cho dự án Laravel:
includes:
- vendor/larastan/larastan/extension.neon
- phpstan-baseline.neon
parameters:
level: 5
paths:
- app
- config
- routes
- tests
tmpDir: build/phpstan # result cache — nhớ cache thư mục này trên CI
1.5. Baseline — cách áp dụng vào repo cũ mà không chết
Repo 5 năm tuổi chạy PHPStan level 5 lần đầu: 3000 lỗi. Không ai sửa hết được. Baseline giải quyết đúng chuyện đó — đóng băng nợ cũ, siết code mới:
vendor/bin/phpstan analyse --level 5 \
--configuration phpstan.neon \
app/ tests/ --generate-baseline
flowchart LR A["3000 lỗi cũ"] --> B["--generate-baseline
→ phpstan-baseline.neon"] B --> C["CI xanh trở lại"] C --> D["Code MỚI vẫn bị
soi ở level 5"] D --> E["Đụng file cũ →
tiện tay xoá vài dòng baseline"] E --> F["Baseline teo dần"]
Chiêu hay dùng thêm: chạy level cao hơn mức đang enforce rồi baseline hết, để code mới tự động bị soi khắt khe hơn code cũ.
2. Lợi ích
2.1. pint --test
- Diff sạch. Code review chỉ còn thay đổi thật, không lẫn 40 dòng đổi thụt lề vì đồng nghiệp dùng IDE khác. Đây là lợi ích lớn nhất và ít người nói ra.
- Chấm dứt tranh cãi vô nghĩa. Không còn bàn về dấu ngoặc xuống dòng hay không. Pint quyết, cả team làm theo. Tiết kiệm thời gian review thật.
- Giảm merge conflict. Hai nhánh format khác nhau cùng file → conflict giả.
- Bắt syntax error trong vài giây (qua
getInvalidErrors), trước khi job test tốn vài phút mới báo. - Rẻ khủng khiếp. Vài giây CI, không cần DB, không cần service nào.
- Onboarding nhanh. Người mới không cần đọc tài liệu quy ước code — chạy
pintlà xong.
2.2. Static analysis
- Phủ 100% dòng code mà không cần viết test. Đây là lợi ích không thứ gì
thay thế được. Command chạy 1 lần/tháng, nhánh
catchhiếm khi vào, Artisan command nội bộ — chỗ nào cũng không có test, và chính chỗ đó hay hỏng nhất. - Refactor an toàn hơn hẳn. Đổi tên method, đổi chữ ký hàm, đổi kiểu trả về — PHPStan chỉ ra mọi chỗ gọi bị hỏng. Với PHP không có compiler, đây gần như là thứ duy nhất thay thế được.
- Bắt đúng những lỗi runtime hay gặp nhất trong Laravel:
Call to a member function x() on null, truyềnstringvào chỗ cầnint,find()trả null nhưng dùng thẳng. - Bắt code chết — nhánh
ifkhông bao giờ đúng, biến gán rồi không dùng. Sau vài đợt refactor, lượng code chết nhiều hơn bạn tưởng. - Bắt PHPDoc nói dối (từ level 2). Docblock ghi
@return Usernhưng thực tế trảUser|null— nguồn hiểu lầm kinh điển. - Ép chất lượng dâng dần. Baseline chỉ teo, không phình → code base tự tốt lên.
3. Tác hại / hạn chế
3.1. Hạn chế của pint --test
- Không bắt được một bug nào. Nói thẳng như vậy trong phỏng vấn. Code format hoàn hảo vẫn có thể xoá nhầm bảng production.
- Commit đầu tiên rất to. Áp vào repo cũ → một commit đổi 400 file,
làm hỏng
git blamecủa cả repo (xem cách xử lý ở phần 6.1). - Risky fixer đổi hành vi. Preset
symfonyvà một số rule bật tay được PHP-CS-Fixer đánh dấu risky — chúng sửa cả ngữ nghĩa, không chỉ hình thức. Ví dụ rule đổi==thành===, hay bỏ import "không dùng" mà thật ra có dùng trong string của annotation. Presetlaravelmặc định an toàn, nhưng bật rule tay thì phải đọc kỹ. - Ma sát với IDE. Ai bật format-on-save của PhpStorm với cấu hình khác sẽ đánh nhau với Pint qua từng commit.
- CI đỏ vì lý do vặt. Thiếu một dấu phẩy cuối → pipeline đỏ → dev phải push lại → mất một vòng CI. Gây bực và làm người ta quen với việc "CI đỏ cũng không sao" — đó mới là tác hại thật.
3.2. Hạn chế của static analysis
- Không hiểu được "phép thuật".
__get,__call, container binding động,app('some.key'), dữ liệu từ DB — PHPStan chịu. Larastan vá phần lớn cho Laravel, nhưng code magic tự viết thì vẫn phải thêm stub thủ công. - False positive gây mất niềm tin. Vài lần báo sai là dev bắt đầu rắc
@phpstan-ignore-next-linekhắp nơi. Job vẫn xanh, giá trị bằng không. - Chậm và ngốn RAM. Repo lớn không cache: vài phút và có thể >1GB RAM.
Job hay chết vì
Allowed memory size exhausted. - Baseline dễ bị lạm dụng. Sai lầm kinh điển: mỗi lần CI đỏ lại chạy
--generate-baselinecho nhanh. Baseline phình mãi = tắt static analysis bằng một cách vòng vo. - Không thay được test. Kiểu đúng hết mà công thức tính tiền sai vẫn xanh. PHPStan không biết gì về nghiệp vụ.
- Chi phí học. Generic trong PHPDoc (
@return Collection<int, User>), template type, kiểu conditional — team không quen thì level ≥6 rất mệt.
4. Trade-off
4.1. --test vs bot tự sửa và commit ngược lại
pint --test (CI chỉ báo) | CI chạy pint rồi commit ngược | |
|---|---|---|
| Ai sửa | Dev | Pipeline |
| Rắc rối | Dev phải push thêm 1 lần | Cần token ghi repo; dễ vòng lặp pipeline; commit không ký được; xung đột với protected branch |
| Bảo mật | Không cần quyền ghi | Phải cấp quyền ghi cho CI — thêm bề mặt tấn công |
| Khi nào chọn | Mặc định | Repo rất lớn, team rất đông, đã có bot infra sẵn |
Cách tốt nhất là kết hợp:
pint --dirtyở pre-commit hook để dev gần như không bao giờ thấy CI đỏ vì style, cònpint --testở CI làm chốt chặn cuối. Hook có thể bị--no-verifybỏ qua, nên CI vẫn phải có. Đừng chỉ có hook.
4.2. Chọn level PHPStan nào
| Tình huống | Level nên chọn | Vì sao |
|---|---|---|
| Repo cũ, chưa từng chạy | 5 + baseline | Level 5 đã bắt được lỗi kiểu tham số — giá trị/công sức tốt nhất |
| Dự án mới, team quen type | 8 | Bắt null safety ngay từ đầu, chưa có nợ nên không đau |
| Team chưa quen static analysis | 0 → 2, tăng dần | Ưu tiên team chấp nhận công cụ hơn là bắt được nhiều lỗi |
| Muốn ép code mới sạch hơn code cũ | 8 + baseline lớn | Nợ cũ đóng băng, code mới bị soi ở mức cao |
Chống chỉ định: nhảy thẳng lên level 9/10 trên repo cũ. Ngàn lỗi, cả team ghét công cụ, kết cục là ai đó xoá job khỏi pipeline. Level 5 chạy được còn hơn level 9 bị vô hiệu hoá.
4.3. Đặt job ở đâu trong pipeline
flowchart LR
subgraph S1["stage: quality (song song, ~1-2 phút)"]
P["pint --test"]
ST["phpstan analyse"]
AU["composer audit"]
end
subgraph S2["stage: test (~5-15 phút)"]
T["phpunit / pest
+ PostgreSQL service"]
end
subgraph S3["stage: build"]
B["docker build"]
end
S1 --> S2 --> S3
Trade-off ở đây là fail fast đấu với phản hồi đầy đủ:
| Cách xếp | Được | Mất |
|---|---|---|
| Quality là stage trước test | Tiết kiệm compute (test không chạy nếu style sai) | Dev sửa dấu phẩy xong mới biết test cũng đỏ → hai vòng CI |
| Quality song song với test | Một lần chạy biết hết mọi lỗi | Tốn compute minutes hơn |
Với team nhỏ (3–4 người) thì chạy song song gần như luôn đúng — thời gian của người đắt hơn compute minutes rất nhiều.
4.4. Vậy thiếu nó ở stage test thì sao? (câu hỏi chính)
Đây là chỗ phải trả lời trung thực, đừng thổi phồng:
| Thiếu cái gì | Bug lọt production? | Mất gì thật | Mức nghiêm trọng |
|---|---|---|---|
pint --test | Gần như không | Diff bẩn, review chậm, conflict giả, code base dần lộn xộn, git blame vô dụng | Thấp — nhưng tích luỹ theo thời gian |
| static analysis | Có, thường xuyên | Lỗi null/kiểu nổ ở production, refactor thành trò may rủi, phải viết nhiều test hơn để bù | Cao |
Diễn giải cho câu trả lời phỏng vấn:
- Thiếu
pint --test: hệ thống vẫn chạy đúng. Cái mất là chi phí con người — mỗi MR reviewer phải lọc thay đổi thật giữa nhiễu format. Và quan trọng hơn: quy ước không được enforce = quy ước không tồn tại. Sau 6 tháng, code base có 4 phong cách khác nhau và không ai dám chạypintvì commit sẽ đổi 400 file. Đó là nợ kỹ thuật lãi kép — nhỏ nhưng chỉ tăng. - Thiếu static analysis: mất thật. Test chỉ phủ nhánh có test.
Toàn bộ phần còn lại — command, listener, nhánh lỗi, code xử lý edge case — không ai gác.
Bug
Attempt to read property on nullsẽ ra production, và thường ra vào lúc có dữ liệu bất thường, tức là lúc bận nhất.
Một chỗ dễ bị hỏi ngược: "Nếu
pint --testkhông bắt bug, sao còn thêm vào CI?" — Trả lời: vì nó gần như miễn phí (vài giây, không cần service) và nó bảo vệ khả năng đọc diff. Mà đọc được diff chính là điều kiện để code review bắt được bug thật. Nó không bắt bug trực tiếp, nó làm cho người bắt bug dễ hơn.
5. Khó khăn khi vận hành thực tế
5.1. Cache — không có là job static analysis chậm gấp 5–10 lần
PHPStan có result cache: nhớ kết quả cũ, lần sau chỉ phân tích file đổi. Trên CI, mỗi job là container mới nên cache biến mất nếu không khai báo:
phpstan:
stage: quality
script:
- composer install --no-interaction --prefer-dist
- vendor/bin/phpstan analyse --memory-limit=1G --no-progress
cache:
key: "phpstan-$CI_COMMIT_REF_SLUG"
paths:
- build/phpstan/ # trùng với tmpDir trong phpstan.neon
- vendor/
Bẫy hay gặp: cache key theo branch → branch mới luôn chạy lạnh (chậm). Cách hay dùng là
fallback_keystrỏ về cache củamain. Còn đổiphpstan.neonhoặc nâng version PHPStan thì cache tự bị vô hiệu — đúng như mong muốn.
5.2. Memory limit
PHPStan mặc định theo memory_limit của PHP. Repo lớn dễ gặp
Allowed memory size of X bytes exhausted. Luôn đặt --memory-limit=1G
(hoặc -1 nếu runner có RAM thoải mái). Nếu vẫn chết: giảm level hoặc chia
paths thành nhiều job.
5.3. Áp vào repo đang chạy — thứ tự việc quan trọng
flowchart TD A["1. Merge hết MR đang mở
(giảm conflict)"] --> B["2. Chạy pint 1 lần
commit riêng: 'style: apply pint'"] B --> C["3. Thêm hash commit đó vào
.git-blame-ignore-revs"] C --> D["4. Thêm job pint --test vào CI"] D --> E["5. Chạy phpstan, sinh baseline,
commit baseline"] E --> F["6. Thêm job phpstan vào CI"] F --> G["7. Thoả thuận: baseline chỉ được teo,
không được phình"]
Bước 3 là bước hay bị quên nhất và tiếc nhất:
# .git-blame-ignore-revs
# style: apply pint to whole codebase
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# Bật một lần cho repo:
git config blame.ignoreRevsFile .git-blame-ignore-revs
Không có file này thì git blame mọi dòng đều trỏ về commit format,
mất luôn lịch sử "ai viết dòng này, vì sao" — đúng thứ bạn cần lúc 3h sáng.
GitHub và GitLab đều tự đọc file này.
5.4. Con người mới là phần khó
- Phải chốt: quy ước style không tranh cãi nữa. Ai muốn đổi thì đổi
pint.jsonqua MR, bàn một lần rồi thôi. - Phải chốt luật baseline: chỉ được giảm. Nếu không có luật này thì công cụ vô nghĩa sau 3 tháng. Có thể ép bằng cách kiểm tra số dòng baseline trong CI.
- Người mới cần biết cách chạy ở máy:
vendor/bin/pint --dirtyvàvendor/bin/phpstan analyse. Ghi vào README, không phải trong đầu ai đó.
5.5. Debug lúc 3h sáng
- Job đỏ mà không hiểu vì sao:
pint --test -vin ra chính xác rule nào bị vi phạm ở file nào. - PHPStan đỏ trên master mà không ai đổi code: gần như luôn là do
composer updatekéo version thư viện mới có type chặt hơn. Kiểm tracomposer.lockvừa đổi chưa. Đây là lý do nên commitcomposer.lockvà cài bằngcomposer installchứ không phảiupdatetrên CI. - Chạy local xanh, CI đỏ: khác version PHP giữa máy dev và runner. Ép
phpVersiontrongphpstan.neonđể hai bên phân tích giống nhau.
6. Case thực tế và cách xử lý
Case 1 — Thêm pint --test vào repo 3 năm tuổi, CI đỏ 380 file
Tình huống: thêm job, pipeline đỏ ngay, 380/500 file sai style. Có 6 MR đang mở.
Xử lý theo bước:
- Không merge job vào master ngay. Tạo branch riêng.
- Báo team dừng tạo MR mới trong nửa ngày; merge hoặc rebase hết MR đang mở trước.
- Chạy
vendor/bin/pintmột lần trên toàn repo. - Commit riêng biệt, không lẫn thay đổi logic:
style: apply pint (laravel preset) to whole codebase. - Đọc lướt diff xem có rule nào đổi hành vi không (chú ý import bị xoá,
so sánh
==→===). Nếu presetlaravelmặc định thì gần như không có, nhưng vẫn phải liếc qua. - Thêm hash commit vào
.git-blame-ignore-revs. - Merge, rồi mới bật job
pint --test. - Ai còn MR dở:
git rebase masterrồi chạyvendor/bin/pint --dirtyđể dọn phần conflict.
Sai lầm cần tránh: gộp commit format với commit tính năng. Reviewer sẽ không thể tìm ra 5 dòng logic giữa 380 file đổi format, và bug lọt qua ngay lần đó.
Case 2 — Bug null lọt production ở đúng chỗ test không phủ
Tình huống: Sản phẩm mới thêm cột brand_id nullable.
Code hiển thị viết:
public function render()
{
$product = Product::find($this->productId);
return view('product.detail', [
'brandName' => $product->brand->name, // brand có thể null
]);
}
Test chạy xanh vì factory luôn tạo sản phẩm có brand. Production có 200 sản phẩm cũ
không brand → Attempt to read property "name" on null → trang 500.
PHPStan level 8 + Larastan bắt được ngay ở CI:
Cannot access property $name on App\Models\Brand|null.
Xử lý:
- Trước mắt: sửa thành
$product->brand?->name ?? 'Không rõ', deploy hotfix. - Sau đó: khai báo quan hệ đúng kiểu để PHPStan hiểu —
@return BelongsTo<Brand, Product>và nullable ở nơi phù hợp. - Bật PHPStan level 8 cho thư mục
app/Livewiretrước (chia nhỏ theo path), baseline phần còn lại — thay vì cố lên level 8 cả repo cùng lúc. - Thêm test cho sản phẩm không brand — nhưng đây là bổ sung, không phải giải pháp chính. Không ai nhớ viết test cho mọi tổ hợp null.
Case 3 — Baseline phình từ 300 lên 900 lỗi trong 4 tháng
Tình huống: mỗi lần CI đỏ, dev chạy --generate-baseline cho nhanh
rồi commit. Job vẫn xanh nhưng không còn bắt được gì.
Xử lý:
- Chặn ở CI: kiểm tra baseline không được nhiều dòng hơn bản trên
main. Vài dòng script là đủ, không cần tool riêng. - Xoá baseline, sinh lại từ đầu để biết con số thật hiện tại.
- Thoả thuận rõ với team: đụng vào file nào thì dọn lỗi baseline của file đó (boy scout rule). Không đặt mục tiêu "sửa hết trong sprint này" — sẽ không xảy ra.
- Ai thật sự cần bỏ qua một lỗi cụ thể thì dùng
@phpstan-ignore-next-linekèm comment giải thích ngay tại chỗ — nhìn thấy trong review, khác hẳn với việc giấu vào file baseline.
Case 4 — Pint đánh nhau với dev, người ta bắt đầu dùng --no-verify
Tình huống: pre-commit hook chạy pint toàn repo, mất 25 giây
mỗi lần commit. Dev bực, ai cũng git commit --no-verify, CI đỏ liên tục.
Xử lý:
- Đổi hook sang
vendor/bin/pint --dirty— chỉ file đang sửa, còn dưới 1 giây. - Trên CI dùng
--diff $CI_MERGE_REQUEST_TARGET_BRANCH_NAMEcho MR, quét toàn bộ chỉ trên nhánh chính. - Giữ
pint --testở CI làm chốt chặn — hook luôn có thể bị bỏ qua, đừng bao giờ chỉ dựa vào hook.
Bài học chung: công cụ chất lượng bị vô hiệu hoá vì chậm hoặc ồn, gần như không bao giờ vì người ta không đồng ý với nó. Tối ưu cho tốc độ phản hồi quan trọng hơn tối ưu cho độ phủ.
7. Câu hỏi phụ interviewer hay hỏi tiếp
| Câu hỏi | Trả lời ngắn |
|---|---|
| Pint khác PHP-CS-Fixer chỗ nào? | Pint bọc PHP-CS-Fixer, cấu hình sẵn theo preset Laravel, dùng JSON thay vì file PHP, và ship sẵn trong Laravel 9+. Bên dưới vẫn là PHP-CS-Fixer. |
| Static analysis có thay được test không? | Không. Nó kiểm tra kiểu và cấu trúc, không biết nghiệp vụ. Hàm tính thuế sai công thức vẫn xanh hết. Hai thứ phủ hai chiều khác nhau: static phủ rộng-nông, test phủ hẹp-sâu. |
| Chọn level PHPStan nào cho dự án Laravel? | Bắt đầu level 5 + Larastan + baseline. Level 8 nếu là dự án mới. Nhảy thẳng lên 9/10 trên repo cũ là cách nhanh nhất để cả team bỏ công cụ. |
Vì sao không để CI tự chạy pint rồi commit lại? |
Cần cấp quyền ghi repo cho CI (rủi ro bảo mật), dễ tạo vòng lặp pipeline, xung đột với
protected branch và commit ký. --test + pre-commit hook giải quyết cùng vấn đề mà không có mấy cái đó. |
| Baseline có phải là gian lận không? | Không, nếu nó chỉ teo. Nó là cách nói "nợ cũ đóng băng, code mới phải sạch". Thành gian lận khi nó phình — nên cần luật và cần kiểm tra tự động. |
| Đặt job này trước hay song song với test? | Team nhỏ: song song, để một lần chạy biết hết lỗi. Trước test chỉ hợp lý khi compute minutes đắt hơn thời gian chờ của dev. |
Còn composer audit thì sao? |
Là tầng thứ ba trong stage quality: quét dependency có CVE đã biết. Rẻ như Pint, nhưng bắt lỗi bảo mật thật. Nên có cùng chỗ. Chú ý advisory mới có thể làm master đỏ dù không ai đổi code — cần thoả thuận trước cách xử lý. |
| Nếu chỉ được thêm 1 job, chọn cái nào? | Static analysis. Pint bảo vệ khả năng đọc code; PHPStan bảo vệ production. |
| Làm sao biết static analysis có ích thật hay chỉ làm đẹp CI? | Đo bằng số lỗi baseline giảm theo tháng, và số lần PHPStan chặn được lỗi trước khi lên production (đếm thủ công trong retro cũng được). Nếu baseline đứng yên và không ai nhớ nó từng bắt được gì thì đúng là chỉ để làm đẹp. |
Tóm tắt để nhớ
flowchart TD Q["Thiếu ở stage test
thì sao?"] --> A["pint --test"] Q --> B["static analysis"] A --> A1["Không mất bug
Mất: diff bẩn, review chậm,
quy ước tự tan rã"] B --> B1["MẤT BUG THẬT
Ở đúng nhánh code
không có test phủ"] A1 --> C["Nên có — vì gần như miễn phí"] B1 --> D["Phải có"]
Liên quan: Deploy Laravel bằng GitLab CI/CD (cấu trúc stage, cache, compute minutes) và Git workflow (chỗ đặt hook và protected branch).