4: Syntax & Structure
Phlo compiles .phlo source files to plain PHP classes and generated assets. The syntax is PHP-like, but without semicolons at the end of statements.
4.1: File structure
A .phlo file can contain top-level controller code and nodes:
routefunctionpropstaticmethodview<style><script>
Example:
route both GET home => view($this)
prop title = 'Welcome'
view:
<h1>$this->title</h1>4.2: Statements and the line parser
Statements end at a line break, not at a semicolon:
$count = 1
if ($active) $count++
This works because the compiler appends a ; to every line and only removes it where the line clearly continues. The full rule set:
- Every line gets a
;at the end. - That
;is removed when the line ends with(,[,{,},,or.. These are implicit continuations: an opening bracket, a trailing comma in an argument list, or a concatenation that breaks after the.. - A line that ends with a single backslash
\is an explicit continuation: the parser removes the backslash and the semicolon and joins the line with the next one. Use this for anything that does not fall under the implicit set, such as a multiline ternary:
$label = $visitors === 1 \
? 'visitor' \
: 'visitors'
- Empty lines never get a
;.
The mental model: stop thinking about semicolons entirely. Only ask "is my statement complete on this line?" If not, end the line with one of ( [ { , . (which happens naturally in most multiline code) or with an explicit \.
Without an implicit or explicit continuation, every line becomes its own statement. That is why in multiline calls every argument line must end with a comma, including the last one:
apply (
title: 'Done',
main: '<p>Ready</p>',
)
Lesson. The file-level node parser tracks multiline node bodies by counting parentheses, not square brackets. A multiline
prop x => [ ... ]ends the node after its first line and the remaining lines become stray controller code (Controller must be in one place). Open multiline node bodies with a parenthesis:prop x => arr(...)orprop x => array_merge(...).
4.3: Controller code
Top-level code that is not a node becomes controller code of the generated class. That code runs when the instance is first retrieved.
prop ready = false
$this->ready = true
Use controller code for light initialization. Put request logic in routes and methods instead.
4.4: Instances
Use %name to retrieve a Phlo object through the instance manager:
%payload->name
%session->user
%creds->mysql->database
The instance is created lazily and then reused within the request.
Lesson. The compiler rewrites
%nameEVERYWHERE in a.phlofile, including inside string literals. A page that tried to print the literal text%sessionin a code example shippedphlo('session')to its visitors. Example code that must stay verbatim belongs in external files (.txt,.md) loaded at runtime, never in.phlostring literals.
4.5: Props
Static prop:
prop title = 'App'
Computed prop:
prop fullName => $this->first.' '.$this->last
Computed props are generated as getters and can be read as a property:
$this->fullName4.6: Methods
Single-line method:
method label($value) => ucfirst($value)
Multiline method:
method label($value){
if (!$value) return void
return ucfirst($value)
}4.7: Functions
You define global functions with function:
function initials($name){
return strtoupper(substr($name, 0, 1))
}
Use this sparingly. For app code a regular app class is usually better; only framework-wide helpers belong in a runtime resource.
4.8: Statics
Static value:
static table = 'users'
Static method:
static label($value) => ucfirst($value)
Call:
user::label('jordi')4.9: Strings and operators
Strings and operators follow PHP:
$name = 'Jordi'
$title = "Hello $name"
$active = $count > 0 && !$archived
Use void for an empty string when that makes the intent clear.
4.10: Named arguments
Named arguments work as in PHP and keep calls readable:
%DB->load (
table: 'users',
where: 'active=1',
order: 'name',
)4.11: Error handling and JSON responses
error('message', $code = 500) aborts a request with a status. It throws (no return needed), and the engine renders it to fit the request:
- an async/SPA request gets
apply(error: 'message')(phlo.js shows it); - a JSON context (the route called
%security->api, or the client sentAccept: application/json) gets a JSON body{"error": "message"}with the status code; - otherwise the HTML error page (the full debug page when
debug: true, a minimal page otherwise).
if (!$record) error('Record not found', 404)
Client errors ($code < 500) keep their message; server errors (>= 500) stay generic ("Error") unless debug: true, so uncaught-exception internals are not exposed by default.
For a successful JSON response use output($data, code:): an array is encoded automatically and code sets the status. A JSON API route therefore uses output() for results and error() for failures, with no per-app response wrapper.
debug: true (set in www/app.php) enables verbose debug output; runtime errors are logged to data/errors.json.