Mấy hôm nay mình gặp lại một kiểu callback khá quen: chỉ muốn cố định một đối số, nhưng vẫn phải bọc cả lời gọi trong arrow function. Bài này giúp các bạn hiểu và tự thử partial function application PHP 8.6, rồi quyết định lúc nào cú pháp mới thực sự dễ đọc hơn. Mình không chỉ tóm tắt RFC: mình đã chạy binary PHP 8.6.0 Alpha 3 chính thức trên Windows, đối chiếu Reflection, lỗi runtime và PHPStan. Giới hạn quan trọng nhất là Alpha 3 chỉ dành cho kiểm thử, chưa được dùng ở production; tooling cũng chưa theo kịp hoàn toàn.
Kết quả ngắn gọn: dấu ? giữ đúng một vị trí và trả về Closure; dấu ... chuyển tiếp phần đối số còn lại. Cú pháp này rất gọn cho callback một dòng và pipe, nhưng closure cũ vẫn sáng sủa hơn khi logic có nhánh, cần debug hoặc dự án còn hỗ trợ PHP cũ.
Mục lục [Hiển thị]
- Vì sao callback PHP lại cần thêm một cú pháp mới?
- Môi trường Alpha 3 mình dùng để thử
- Partial function application PHP 8.6 hoạt động ra sao?
- So với arrow function và first-class callable
- Mình thử ba tình huống gần code Laravel
- PFA đi cùng pipe operator có dễ đọc hơn không?
- Tooling nào theo kịp Alpha 3?
- Khi nào mình dùng, khi nào vẫn chọn closure cũ?
Vì sao callback PHP lại cần thêm một cú pháp mới?
Ví dụ mình muốn biến một chuỗi thành slug. Hàm có hai đối số, còn callback chỉ nhận chuỗi đầu vào. Với PHP hiện tại, đoạn quen thuộc sẽ như sau:
function slugify(string $value, string $separator = '-'): string
{
return strtolower(str_replace(' ', $separator, trim($value)));
}
$callback = static fn(string $value): string => slugify($value, '-');
echo $callback(' Hello World '); // hello-world
Arrow function ở đây không sai. Nó chỉ hơi “làm thủ tục”: nhận $value, chuyển nguyên xi vào slugify(), còn dấu gạch ngang đã được cố định. Partial function application, viết tắt là PFA, cho phép đặt placeholder ngay ở vị trí còn thiếu:
$callback = slugify(?, '-');
var_dump(get_debug_type($callback)); // string(7) "Closure"
echo $callback(' Hello World '); // hello-world
Điểm quan trọng là PHP chưa gọi slugify() ở dòng đầu. Engine tạo một closure, giữ sẵn dấu -, rồi chờ giá trị cho vị trí ?. Nó gần partial application hơn currying: mình cố định một phần danh sách đối số trong một lần, chứ không biến mọi tham số thành chuỗi hàm một đối số.

Môi trường Alpha 3 mình dùng để thử
Mình tải bản VS18 x64 Non Thread Safe từ trang pre-release chính thức, đặt riêng trong thư mục lab và không thay PHP của Herd hay website. File ZIP có SHA-256 ac3a4254...3071417, khớp manifest trên php.net. Runtime trả về:
PHP 8.6.0alpha3 (cli) (built: Jul 28 2026 13:41:09) (NTS Visual C++ 2026 x64)
Zend Engine v4.6.0-dev
Loaded Configuration File: (none)
Mình chạy ngày 01/08/2026 trên Windows x64, không nạp php.ini. File lab được kiểm tra bằng php -l trước, sau đó chạy các assertion cho function, static method, object method, named argument, optional parameter, variadic và pipe. Tất cả assertion đều pass. Cách tách runtime này cùng tinh thần với việc chạy công cụ PHP trong môi trường cô lập: thử đồ mới thì vui, nhưng đừng để production lãnh đủ.
PHP công bố Alpha 3 ngày 30/07/2026 và dự kiến Beta 1 vào 13/08. Chính trang phát hành ghi rõ đây là bản test sớm, không dùng production. Mình nhắc hơi nhiều vì chữ “implemented” trong UPGRADING rất dễ tạo cảm giác tính năng đã sẵn sàng cho mọi dự án. Đời không như mơ nhanh vậy.
Partial function application PHP 8.6 hoạt động ra sao?
Theo RFC v2, chỉ cần một lời gọi có ? hoặc ..., PHP sẽ tạo closure thay vì thực thi hàm ngay. Closure mới kế thừa tên, kiểu, cách truyền tham chiếu và kiểu trả về của các tham số chưa được điền. Mình kiểm tra bằng Reflection chứ không đoán từ cú pháp:
$callback = slugify(?, '-');
$reflection = new ReflectionFunction($callback);
foreach ($reflection->getParameters() as $parameter) {
printf(
"%s | %s | required=%s\n",
$parameter->getName(),
$parameter->getType(),
$parameter->isOptional() ? 'no' : 'yes',
);
}
// value | string | required=yes
Placeholder ? giữ đúng một vị trí
Mỗi ? tương ứng đúng một đối số. Nó có thể nằm đầu, giữa, cuối hoặc trong named argument. Hai placeholder sẽ tạo closure có hai tham số theo đúng type của hàm gốc:
final class Formatter
{
public static function wrap(
string $value,
string $left,
string $right,
): string {
return "$left$value$right";
}
}
$wrap = Formatter::wrap(?, '[', ?);
echo $wrap('PHP', ']'); // [PHP]
Một thay đổi dễ bị các bài cũ mô tả sai nằm ở optional parameter. RFC bổ sung tháng 5/2026 đã chốt rằng mọi vị trí dùng ? đều trở thành required, kể cả tham số gốc có default. Trong lab, describe('guide', ?) tạo tham số prefix kiểu string và required=true. Gọi closure mà không truyền gì nhận ArgumentCountError, đúng với Alpha 3.
function describe(
string $value,
string $prefix = 'post',
string $suffix = 'html',
): string {
return "$prefix:$value.$suffix";
}
$named = describe(value: ?, suffix: 'json');
echo $named('guide'); // post:guide.json
$requiredPrefix = describe('guide', ?);
$requiredPrefix(); // ArgumentCountError
Placeholder ... chuyển tiếp phần đối số còn lại
Dấu ... không có nghĩa “chính xác một đối số”. Nó nhận zero hoặc nhiều đối số còn lại. Đây cũng là chỗ dễ nhầm với first-class callable foo(...): cùng ký hiệu, nhưng khi có thêm giá trị đã điền trước, nó trở thành partial application.
function combine(string $glue, string ...$parts): string
{
return implode($glue, $parts);
}
$joinWithSlash = combine('/', ...);
echo $joinWithSlash(); // chuỗi rỗng
echo $joinWithSlash('a'); // a
echo $joinWithSlash('a', 'b', 'c'); // a/b/c
Reflection cho closure trên trả một tham số parts, type string, variadic=true và không required. RFC còn cảnh báo ... có thể chuyển tiếp đối số thừa vào optional parameter của hàm gốc. Vì vậy callback dùng trong array_filter() hoặc array_find() cần được nhìn cả signature, không chỉ thấy code ngắn là oke.

So với arrow function và first-class callable
Mình cho cùng input chạy qua ba cách viết và assert output giống hệt nhau:
$arrow = static fn(string $value): string => slugify($value, '-');
$partial = slugify(?, '-');
$firstClass = slugify(...);
assert($arrow($input) === 'partial-function-application');
assert($partial($input) === $arrow($input));
assert($firstClass($input) === $arrow($input));
| Cách viết | Phù hợp nhất | Điểm cần nhớ |
|---|---|---|
| First-class callable | Giữ nguyên toàn bộ signature | Không cố định đối số nào |
| Arrow function | Biến đổi ngắn nhưng có biểu thức riêng | Rõ với team đang dùng PHP cũ |
| PFA | Cố định vài đối số, giữ type còn lại | Cần PHP 8.6 và parser hỗ trợ |
PFA không “thay thế closure”. Nó là cách viết một nhóm closure chuyển tiếp đối số. Closure đầy đủ vẫn cần thiết khi có nhiều câu lệnh, bắt biến theo ngữ cảnh, log, xử lý exception hoặc đặt breakpoint ở giữa logic.
Mình thử ba tình huống gần code Laravel
Mình không dựng một project Laravel rồi tuyên bố framework đã hỗ trợ Alpha 3. Thay vào đó, mình viết PHP thuần theo ba mô-típ hay gặp trong app Laravel: chuẩn hóa slug, lọc collection-like array và tạo callback từ service method. Như vậy mình kiểm tra đúng semantics ngôn ngữ mà không đánh tráo thành chứng nhận tương thích framework.
$slugger = slugify(?, '-');
echo $slugger(' Hello World '); // hello-world
$entries = [
['slug' => 'first', 'status' => 'draft'],
['slug' => 'second', 'status' => 'published'],
];
$filter = new EntryFilter();
$publishedOnly = $filter->matches(?, 'published');
$published = array_values(array_filter($entries, $publishedOnly));
Kết quả lọc giữ lại second. Object method PFA mang theo object đã bind, còn Reflection vẫn biết tham số đầu là array. Với service có callback một đối số và cấu hình cố định, mình thấy cách này khá sáng. Nhưng nếu callback còn kiểm tra quyền, ghi log và đổi exception, mình vẫn đặt tên closure riêng để lúc trace không phải chơi trò truy tìm kho báu.
PFA đi cùng pipe operator có dễ đọc hơn không?
Pipe operator đã có từ PHP 8.5 và là một lý do PFA được quan tâm. Mình chạy pipeline trim → thay khoảng trắng → lowercase trên Alpha 3:
$result = $input
|> trim(?)
|> str_replace(' ', '-', ?)
|> strtolower(?);
echo $result; // partial-function-application
Với ba bước unary rõ nghĩa, mình thích phiên bản pipe: dữ liệu đi từ trên xuống, không có biến trung gian và cũng không lồng ba cặp ngoặc. Tuy nhiên lợi ích giảm nhanh nếu mỗi bước cần hai placeholder, named argument khó đoán hoặc có side effect. Khi review, mình ưu tiên câu hỏi “đọc từ trái sang phải có biết dữ liệu vào chỗ nào không?” hơn việc đếm ít hơn vài ký tự.
Tooling nào theo kịp Alpha 3?
PHP 8.6 Alpha 3 tự parse và chạy file lab bình thường. Nhưng khi mình tải PHPStan bản phát hành mới nhất tại thời điểm thử, PHPStan 2.2.7, rồi phân tích file PFA tối thiểu ở level 5, kết quả là:
Line 10: Syntax error, unexpected '?'
Identifier: phpstan.parse (non-ignorable)
Result is incomplete because of severe errors.
Đây không phải lỗi code runtime; nó cho thấy parser của tool chưa hiểu cú pháp mới. File composer.json nhánh Laravel 13.x hiện khai báo PHP ^8.3 và có polyfill PHP 8.6, nhưng constraint đó không đồng nghĩa mọi formatter, static analyzer, IDE plugin và package trong một project đã hiểu PFA. Vì vậy mình chỉ gọi ba ví dụ trên là “Laravel-style”, không gọi là kiểm thử tích hợp Laravel.
Chỗ này giống câu chuyện kiểm tra chất lượng code Laravel: runtime cho output đúng mới là lớp đầu. CI, static analysis, convention và khả năng cả team đọc được mới quyết định đoạn code có nên merge hay không.
Khi nào mình dùng, khi nào vẫn chọn closure cũ?
| Tình huống | Lựa chọn của mình | Lý do |
|---|---|---|
| Callback một dòng, cố định một đối số | PFA sau khi lên PHP 8.6 stable | Ngắn và Reflection giữ type |
| Pipeline nhiều bước unary | PFA với pipe | Luồng dữ liệu dễ nhìn |
| Logic có nhánh, log hoặc exception | Closure có tên biến rõ | Dễ debug và review hơn |
| Package hỗ trợ PHP cũ | Arrow function hoặc closure | Không phá compatibility |
| CI chưa parse được cú pháp | Chưa merge PFA | Tránh tắt static analysis để chạy cho qua |
Với mình, PFA giải quyết đúng một nỗi khó chịu có thật: wrapper callback chỉ làm nhiệm vụ chuyển tiếp. Alpha 3 cho thấy semantics đã chạy được, kể cả optional placeholder mới, named argument, object method và pipe. Nhưng lỗi PHPStan 2.2.7 là lời nhắc rất thực tế rằng thời điểm “ngôn ngữ có tính năng” và thời điểm “dự án nên dùng tính năng” không phải lúc nào cũng trùng nhau.
Nếu các bạn muốn thử, hãy dùng runtime tách biệt, giữ test so sánh với closure cũ và kiểm tra toàn bộ CI trước. Còn production thì chờ stable cùng toolchain đã xanh. Những bài toán nhiều bước như vòng lặp đệ quy trong Laravel vẫn nên ưu tiên code có tên và luồng điều khiển rõ; đừng dùng cú pháp mới chỉ vì nó mới.
Nguồn chính và ngày kiểm tra 01/08/2026:
- PHP 8.6.0 Alpha 3 available for testing — PHP.net, 30/07/2026.
- UPGRADING của tag php-8.6.0alpha3 — php-src.
- PHP RFC: Partial Function Application v2 — semantics và giới hạn.
- RFC xử lý optional placeholder — kết quả bỏ phiếu 25-0-1.
- PHP 8.6 kicks off with partial function application — PHP Foundation, 08/12/2025.