plxphp: the PHP dialect¶
plxphp lets you write PostgreSQL functions with PHP syntax. At CREATE FUNCTION
time plx transpiles the body to plpgsql and stores the plpgsql in
pg_proc.prosrc. The function runs on the standard plpgsql interpreter.
The language name is plxphp, so it does not collide with the native PL/PHP.
Setup¶
Function basics¶
Blocks are brace-delimited and statements end with ;. A scalar function ends
with an explicit return.
Function arguments are referenced with the $ sigil, which is removed in the
generated plpgsql. Any SQL expression is valid, because expressions are passed
through to plpgsql and SQL unchanged.
Local variables and types¶
Assignment creates a local. plx infers the type from the first value when it is
an integer, numeric, text, or boolean literal. Otherwise annotate the variable
with a /*:: type */ comment placed before the ;.
$n = 42; // inferred integer
$label = "count"; // inferred text
$amount /*:: numeric */;
$amount = $principal * $rate;
The annotation text is emitted verbatim, so %TYPE and %ROWTYPE work:
A constant uses the const suffix in the annotation:
Control flow¶
if / elseif / else¶
else if is also accepted.
switch¶
switch lowers to plpgsql CASE. Stacked labels become a value list. Each arm
must end with break or a terminating statement (return/throw); genuine
fall-through is rejected.
Loops¶
Counting for and while:
continue and break map to CONTINUE and EXIT.
Loop labels¶
outer: for ($i = 1; $i <= $n; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($i + $j >= $limit) { break outer; }
}
}
Working with data¶
Iterating a query¶
foreach (query("SELECT id, amount FROM orders WHERE grp = {$g}") as $row) {
$total = $total + $row->amount;
}
The row variable is a record. Field access is $row->col or $row['col']. For
bind parameters or non-literal SQL, pass extra arguments:
query($sql, $a) as $row.
Iterating an array¶
The loop variable must be annotated with its element type.
Fetching one row¶
fetch_one returns all-NULL on no row. fetch_one! raises on zero or more than
one row.
Running SQL¶
perform("UPDATE counters SET n = n + 1 WHERE id = {$cid}");
execute("INSERT INTO t(msg) VALUES ($1)", $note);
return_query("SELECT id FROM vip ORDER BY id"); // set-returning function
Use bind arguments in execute for untrusted input.
Cursors¶
$c = open_cursor("SELECT v FROM t ORDER BY v");
$row = fetch_from($c);
while (found()) {
$total = $total + $row->v;
$row = fetch_from($c);
}
close_cursor($c);
move_cursor($c) and move_cursor($c, $n) map to MOVE.
Diagnostics¶
Set-returning functions¶
CREATE FUNCTION squares(n int) RETURNS SETOF int LANGUAGE plxphp AS $$
for ($i = 1; $i <= $n; $i++) { return_next($i * $i); }
return;
$$;
Errors¶
Throwing¶
The call form raise("notice", "message") emits a RAISE at the named level
(notice, warning, info, log, debug, exception).
Handling¶
try {
execute("INSERT INTO uniq(id) VALUES ($1)", $k);
} catch (\Exception $e) {
raise("notice", "dup on " . $e->constraint);
} finally {
perform("INSERT INTO log(msg) VALUES ('done')");
}
catch (\Exception $e) maps to WHEN OTHERS. To catch a specific condition,
name it with the PG:: spelling, for example catch (PG::UniqueViolation $e),
which maps to unique_violation.
Accessors: $e->message to SQLERRM, $e->sqlstate to SQLSTATE, and
$e->detail, $e->hint, $e->constraint, $e->column, $e->table,
$e->schema, $e->datatype to the matching GET STACKED DIAGNOSTICS fields.
Assertions¶
Expressions¶
- Interpolation:
"total is $amount"and"count {$n}"splice the value. Single-quoted strings do not interpolate. - Concatenation:
$a . $bmaps to$a || $b. - Comparison:
==,===,!=,!==map to=and<>.$x == nullbecomes$x IS NULL. - Boolean:
&&,||,!map toAND,OR,NOT. - Ternary:
$c ? $a : $bbecomesCASE WHEN $c THEN $a ELSE $b END. nullbecomesNULL.
Building strings in a loop¶
Concatenating onto a string in a loop is slow in plpgsql: s := s || 'x' is
O(n^2) because text is immutable and each step copies the whole string. Use the
append assignment .=, which plx lowers to its string builder (plx_strbuild):
$s = "" /*:: text */;
foreach (query("SELECT name FROM t ORDER BY id") as $row) {
$s .= $row->name;
$s .= ",";
}
return $s;
On PostgreSQL 18 this is amortized O(1) per append. On PostgreSQL 13 to 17 it is correct but not accelerated (the in-place optimization needs a PostgreSQL 18 feature).
Trigger functions¶
A function returning trigger can be used as a trigger. Assign to $NEW fields
and return $NEW:
CREATE FUNCTION stamp() RETURNS trigger LANGUAGE plxphp AS $$
$NEW->tag = "row {$NEW->id}";
return $NEW;
$$;
$NEW, $OLD, and the TG_ variables are available. Assigning to a record
field works with the arrow form ($NEW->col = e) or the array-element form
($NEW['col'] = e).
Semantic differences¶
These are intentional. plx pins semantics to SQL and plpgsql.
- Decimal literals infer
numeric, not a floating-point type. - Comparisons use SQL three-valued logic:
==,===,!=,!==map to=and<>, so a comparison involving NULL is unknown. Only a comparison with the literalnullbecomesIS NULL/IS NOT NULL. A positiveif/whilecondition treats NULL as false. ===and!==do not preserve PHP strict-type semantics; they behave like==and!=and use SQL type resolution."1" === 1is false in PHP but compares as equal here. Compare like-typed values.- Interpolating a NULL renders as an empty string; the whole string is never made NULL.
- Integer division and modulo follow SQL (truncate toward zero).
- PHP truthiness is not emulated. A condition must be a boolean expression. A non-boolean condition is an error reported by plpgsql when the function runs.
- Locals are function-scoped, matching PHP function scope.
Not supported¶
Rejected at CREATE FUNCTION time with a line number:
- Function or class definitions, namespaces,
use, includes. - Closures and anonymous functions.
- Array and object literals as general values.
- Non-counting
for; onlyfor ($v = LO; $v < HI; $v++)andfor (...; $v += K)are supported. foreachover an array key/value pair (only value iteration).switchfall-through (end each case withbreakorreturn).- Per-block local
DECLARE(locals are function-scoped by design).
See PARITY.md for the full plpgsql construct matrix and ARCHITECTURE.md for how plx maps to the plpgsql engine.