On 07/05/2026 13:16, Bob Weinand wrote:
I am not opposed at all to such a consistent macro system though.
... but I would not make a transaction like that a macro. For inlining
variables in a html or sql snippet with proper escaping, definitely great.
To me, macros should meaningfully transform their contained code rather than
wrapping a bit of logic around, which would be covered by this RFC.
It's complimentary, not a replacement.
Thinking about this further, I agree that full-blooded macros would be
overkill for this case, but my starting point was that closures don't
feel like the right solution either - they're a different feature being
bent into shape.
So rather than starting with a Closure, and taking things away, I was
trying to think of ways to start with a simple block of code, and build
it up.
For instance, the array_walk example in the RFC is just an ugly (and
probably inefficient) way of writing a foreach loop:
array_walk($numbers, fn($number) {
$count = bcadd($count, '1');
$sum = bcadd($sum, $number);
});
foreach($numbers as $number) {
$count = bcadd($count, '1');
$sum = bcadd($sum, $number);
}
So, what would it look like to generalise that, so we could do the same
for other scenarios, like async() and transaction()?
A simple first step would be something that could take a block of code,
and say when to execute it:
custom_block transaction($dbConnection) {
try {
$dbConnection->beginTransaction();
__execute_block_body();
}
catch (\Throwable $e) {
$dbConnection->rollbackTransaction();
throw $e;
}
$dbConnection->commitTransaction();
}
transaction($myDb) {
$myDb->query('UPDATE ...');
}
If the block body is never represented as a variable, there's no need to
define what happens when it outlives scope, or the user tries to clone
it, rebind it, etc.
And if the calling code doesn't look like it's creating a Closure, users
won't have any wrong expectations about variable scopes or lifetimes.
Onto that, we can add syntax to push values into the block, e.g. using
the "as" keyword like in "foreach" (example based on the "Generator
decorator managers" section at the end of the Context Managers RFC):
custom_block opening($filename) {
$f = fopen($filename, "r");
if (!$f) {
throw new Exception("fopen($filename) failed");
}
try {
__execute_block_body($f);
} finally {
fclose($f);
}
}
opening(__FILE__ as $f) {
var_dump($f);
}
And some way to pull values out, e.g. a "break with" keyword:
custom_block transaction(DBConnection $conn) {
$conn->beginTran();
try {
if (__execute_block_body() === DBConnection::TRANSACTION_ABORT) {
$conn->rollbackTran();
return;
}
} catch (\Throwable $e) {
$conn->rollback();
throw $e;
}
$conn->commitTran();
}
transaction($connection) {
$affectedRows = $connection->query("UPDATE ...");
if ($affectedRows === 0) {
break with DBConnection::TRANSACTION_ABORT;
}
// ...
}
I'm sure there are details here that wouldn't quite work as expressed,
but hopefully it explains what I meant by wanting something closer to
macros than closures.
Regards,
--
Rowan Tommins
[IMSoP]