1: Introduction
Phlo is a programming language and engine built on top of PHP 8+, designed to create compact, clear, and performant web apps. Routing, controller code, views, styling, and frontend updates form a cohesive whole. Phlo transpiles .phlo to regular PHP classes and generates the necessary assets.
The web server points to your webroot /www. Unknown paths are rewritten to /www/app.php, where Phlo reads your configuration, manages instances, and handles routes. The build phase runs automatically (JIT) as soon as source files change.
1.1: Philosophy
- Short code, high expressiveness – minimal syntax, no semicolons, compact routes.
- Freedom without condescension – Phlo does not enforce a framework structure.
- Balance in height/width – logical coherence instead of fragmentation across folders.
- Performance through simplicity – transpiled PHP runs without runtime penalty; frontend uses lightweight NDJSON updates.
- One coherent system – routing, rendering, assets, and frontend behavior belong together.
1.2: What is Phlo
Phlo is a superset of PHP with automatic build. A .phlo file can contain:
- Controller code at top-level (executed after instantiation)
- Routes (space-separated)
- Methods & props (including lazy props with
=>) - Views (HTML abbreviations,
<if>,<foreach>) - Styles (compact CSS without semicolons)
- Frontend scripts (bundled together with the view)
Phlo is modularly deployable: full-stack, frontend-engine-only, class-writer/codegen, or as an asset pipeline.
1.3: Installation
1) Place Engine
Place the folder phlo/ in your project (outside /www).
2) Webserver → webroot /www
Set the webserver with document root to /www and rewrite unknown paths to app.php.
Nginx
server {
root /path/to/project/www;
location / {
try_files $uri $uri/ /app.php?$query_string;
}
}
Apache (.htaccess in /www)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ app.php [QSA,L]
Static files (images, app.js, app.css) are served directly; only unknown paths go to Phlo.
3) Content of /www/app.php
Use the working entry point as in your codebase:
<?php
require('/srv/phlo/phlo.php');
phlo_app_jsonfile($app = dirname(__DIR__).'/', "$app/data/app.json");
What this does:
- Loads the engine (
phlo/phlo.php) - Determines the project path (
$app) - Reads and validates
/data/app.json(required) - Initializes instances, executes controller code, and starts routing
4) Build (JIT) and scripts
- The engine performs the build automatically on the next request after a change (JIT).
- If you want to build manually (e.g., in CI), do so via PHP by calling
phlo_build().
1.4: Project structure
A Phlo project has a fixed structure:
/www/ ← webroot (public)
app.php ← central entry point
app.js ← automatically generated frontend bundle
app.css ← automatically generated CSS
/data/app.json ← required configuration
/phlo/ ← Phlo engine
/php/ ← transpiled PHP (automatically)
Important points:
-
app.jsandapp.cssare completely automatically generated from your.phlosources andapp.jsonsettings. → You never write or modify these files manually. → They are overwritten at each build phase. -
The content of these files depends on the
phloNS,defaultNS,phloJS, and CSS build options in/data/app.json. -
/www/app.phpstarts the engine and refers to your configuration. -
/phpcontains the transpiled classes. You also do not modify these files manually; they are generated or refreshed at each build phase.
2: Configuration
Each Phlo app has a mandatory configuration file:
/data/app.json
The engine reads this in from app.php and determines sources, libraries, bundling, and asset output.
2.1: Goal and position
- Location:
/data/app.json(relative to project base). - Loaded by
/www/app.phpvia the Phlo engine. - Strict JSON (no comments, no trailing commas).
- Paths may use the placeholder
%app/(will be replaced by the app path fromapp.php).
2.2: Minimal configuration
{
"id": "MyApp",
"version": ".1",
"host": "localhost",
"dashboard": "phlo",
"debug": true,
"build": {
"libs": []
}
}
Why like this?
build.libsis required (can be an empty array).- All other
buildoptions are optional and you add them when needed (sources, namespaces, CSS/JS build, minify, etc.).
We will handle production/release without (full) build separately later.
2.3: `build.sources`
Only needed if you want more source paths than the app path from /www/app.php.
{
"build": {
"sources": [
"%app/",
"/srv/phloCMS/",
"/srv/phloCMS/fields/"
],
"libs": []
}
}
- Each path can be absolute or start with
%app/. - All
.phloin these paths count for routing, transpiling, and assets.
2.4: `build.libs`
Declare which libraries (from phlo/libs/) you want to preload and have known by your project.
{
"build": {
"libs": [
"DB/DB",
"DB/MySQL",
"model",
"payload"
]
}
}
- Items correspond to files in
phlo/libs/(without extension). - On-demand autoload remains: if your code uses a not-yet-loaded lib, Phlo will load it automatically.
build.libsis therefore your explicit baseline/preload (and may be important for order/initialization).
2.5: Namespaces & assets
Use only if you want to send bundling/asset-scopes.
{
"build": {
"libs": [],
"defaultNS": "app",
"phloNS": ["app", "cms"],
"iconNS": ["cms"],
"icons": "/srv/icons"
}
}
defaultNS– default namespace if there is no explicitns.phloNS– determines which namespaces are included in the frontend bundles and for which namespaces Phlo writes assets to/www/(so broader than justapp.js/app.css: also extra bundles, icon-assets/sprites, etc., depending on available code/assets per namespace).iconNS+icons– namespaces and source for icon sets.
2.6: Frontend build options
{
"build": {
"libs": [],
"phloJS": false,
"buildCSS": true,
"minifyCSS": false,
"buildJS": true,
"minifyJS": false
}
}
| key | type | meaning |
|---|---|---|
phloJS |
bool | Bundle Phlo frontend engine (advanced use cases). |
buildCSS |
bool | Process styles from .phlo and write CSS assets (per namespace). |
minifyCSS |
bool | Minify CSS. |
buildJS |
bool | Bundle frontend scripts and write JS assets (per namespace). |
minifyJS |
bool | Minify JS. |
Important: Everything that Phlo writes to /www/ (such as app.js, app.css and any namespaced/extra assets) is generated and will be overwritten during builds. Do not edit manually.
2.7: Other fields
routes,extends, etc. can be used by modules.%app/in paths is replaced by the current app path during init.- Keep
app.jsonstrictly valid.
3: Syntax & Structure
Phlo is a superset of PHP with compact, semicolon-free syntax. In a single .phlo file, you combine routes, props, methods, views, styles, scripts, and controller code. The builder transpiles to regular PHP/JS/CSS.
3.1: File structure
Top-level elements:
function— project-global functionsroute— routing to methodsstatic— static values or static arrow methods- controller code — top-level statements outside the above blocks
prop— static or computed properties (may have arguments)method— methods on the generated classview Name:— views<script>…</script>— scripts<style>…</style>— styles
Minimal, valid example:
route GET home => $this->main
prop title = 'Welcome'
method main => view($this->home)
view home:
<h1>$this->title</h1>3.2: Controller code
Top-level statements outside of blocks form the controller of the file. The controller runs after the instance exists (not in __construct) — this prevents circular references.
Example (harmless init):
prop initialized = false
$this->initialized = true3.3: Statements
- No semicolons: end of the line closes the statement.
- Multiline arguments: each argument line ends with a comma.
- Spaces around
=>.
Examples:
if ($active) $count = $count + 1
foreach ($rows AS $r) $sum = $sum + $r->value
foreach ($rows AS $r){
$sum = $sum + $r->value
$n = $n + 1
}
chunk (
title: 'Overview',
main: view($this->home),
)
apply (
title: 'Ready',
main: '<p>Done</p>',
)3.4: Variables & Scopes
- Variables like in PHP:
$name. - Global instances via
%Name(e.g.%MySQL,%payload) — lazy via instance manager. - Scope: locals within functions/methods; props/statics on instance/class;
%…shared project-wide.
3.5: Constants
Exact as defined in the engine:
| Constant | Meaning / Value | ||
|---|---|---|---|
phlo |
Current Phlo version (string) | ||
cli |
true if there is no REQUEST_METHOD (CLI) |
||
async |
true if HTTP_X_REQUESTED_WITH equals 'phlo' |
||
method |
'CLI' or the HTTP method of the request |
||
jsonFlags |
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES |
||
br |
'<br>' |
||
bs |
'\\' (backslash) |
||
bt |
'`' (backtick) |
||
colon |
':' |
||
comma |
',' |
||
cr |
"\r" |
||
dash |
'-' |
||
dot |
'.' |
||
dq |
'"' (double quote) |
||
eq |
'=' |
||
lf |
"\n" |
||
nl |
cr.lf → effectively "\r\n" |
||
perc |
'%' |
||
pipe |
' | ' |
||
qm |
'?' |
||
semi |
';' |
||
slash |
'/' |
||
space |
' ' |
||
sq |
"'" (single quote) |
||
tab |
"\t" |
||
us |
'_' |
||
void |
'' (empty string) |
These constants are available everywhere in
.phlo.
3.6: Strings & Operators
Strings: 'single' and "double". Operators: according to PHP. Phlo adds more compact syntax (arrow-bodies, named arguments); semantics remain the same.
3.7: Functions
Functions are project-global and are written to /php/app.php.
Singleline:
function add($a, $b) => $a + $b
Multiline:
function sum($a, $b){
return $a + $b
}3.8: Methods
Methods belong to the class generated from the .phlo file.
method hello($who) => 'Hi '.$who
method classify($x) {
if ($x > 5) return 'large'
return 'small'
}
Arrow for single-line logic; multi-line uses braces.
3.9: Props
Static (transpiles to PHP class property; no function calls allowed):
prop title = 'App'
prop defaults = ['theme' => 'dark']
Computed (lazy + cached):
prop now => time()
prop fullName => $this->first.' '.$this->last
Props with arguments (only computed):
prop repeat($n) => str_repeat('*', $n)
# usage
$this->repeat(5)3.10: Statics
Definition:
static x = 1
static y => time()
Calling within the same class:
dx (
static::$x,
static::x(),
static::y,
)
Calling externally (via class name):
dx (
test::$x,
test::x(),
test::y,
)
Important:
- Props and methods without arguments may be called without
(). - Static methods always require
()when called. - A static value (like
x = 1) may also be called as a method (x()), for consistent syntax.
3.11: Named Arguments
Fully supported; makes calls explicit and less error-prone.
%MySQL->delete (
'Users',
'id=?',
id: 1,
)3.12: Error handling
- Based on PHP's errors/throwables.
debug: trueinapp.jsonactivates extensive debugging via the Phlo engine.
3.13: Style guidelines
- Spaces around
=> - One-liner
if/foreachwithout braces okay; multiline ⇒ with braces - Multiline arguments ⇒ comma at EOL
- Views:
view Name:+ content line(s), no extra indent - No inline braces in CSS
4: Routing
Routing in Phlo associates a space-separated path + HTTP method with a target (usually a method). Routes from all .phlo files are collected; the router is activated with app::route().
4.1: Base form
route [async|both] [GET|POST|PUT|DELETE|PATCH] pad [pad2 ...] => target
- Paths are written with spaces (no
/in the route definition). - Target: a direct call or statement block.
Example:
route GET home => $this->main
method main => view($this->home)4.2: sync / async / both
| Keyword | Behavior |
|---|---|
| (omit) | Only sync (regular HTTP) |
async |
Only async (requests from Phlo-frontend) |
both |
Sync and async allowed |
route both GET data => $this->loadData
route async POST items save => $this->saveItems4.3: Variables
Phlo parses each path segment. Segments that start with $ are variables with extra capabilities:
4.3.1 Required (passed to target)
route GET user $id => $this->showUser($id)
method showUser($id) => view($this->profile)
4.3.2 Optional presence with ? → boolean
route GET search $full? => $this->search($full)
- Matches
/searchand/search/full. $fullis true when the segmentfullis present, otherwise false. (The implementation literally checks if the request segment is equal to the name without?.)
4.3.3 Rest (variable length) with =*
route GET file $path=* => $this->serveFile($path)
- Matches all remaining segments as one string in
$path.
4.3.4 Default value with =
route GET page $slug=home => $this->page($slug)
- Without segment →
$slug = 'home'. - With segment →
$slug = '<value>'.
4.3.5 Length requirement with .N
route GET code $pin.6 => $this->enter($pin)
- Matches only if the length of
$pinis exactly 6.
4.3.6 Choice lists with :a,b,c
route GET report $range:daily,weekly,monthly => $this->report($range)
- The segment must be one of the specified values.
- In absence (empty) and with default, the default is applied.
- Otherwise, the match fails.
You can combine these forms. Examples:
# enum + required id
route GET export $fmt:csv,json $id => $this->export($fmt, $id)
# enum + default
route GET theme $name:light,dark=light => $this->theme($name)4.4: Payload check with `@`
You specify exact body keys with one @ and a comma-separated list. The router compares this 1-to-1 with the keys from %payload (exact set; order as provided by the engine).
route POST user @name,email => $this->createUser
method createUser => dx(%payload->name, %payload->email)
Body keys are not bound as method parameters; you read them via
%payload.
4.5: Targets
Local method
route GET profile show => $this->show
method show => view($this->profile)
External class method (static)
route GET api version $major => api::getVersion($major)
- Static calls: parentheses required, even without arguments.
- Explicitly pass path variables.
4.6: Activate router
Routes are only matched after:
app::route()
Place this call, for example, in app.phlo (or another central controller) after your app initialization and before a fallback for 404 handling.
4.7: Recommended structure
- Place routes at the top per file.
-
Keep path and method name logically aligned:
route GET users list => $this->listUsers route POST users add => $this->addUser - Always pass variables in target.
bothonly when an endpoint intentionally needs to be both sync and async.- Use choice lists
:…instead of separateifbranches for fixed variants.
5: Views
In Phlo, you define views directly in .phlo files. A view is a named or unnamed block that starts with view and contains HTML (plus minimal Phlo constructs).
5.1: Declaration
-
Named
view home: <h1>Welcome</h1> -
Nameless → the name is automatically
viewview: <p>Test</p>
5.2: Arguments
Views can have arguments (including defaults):
view($x = 0):
<p>Value: $x</p>
view detail($input):
<p>Input: $input</p>
Calling:
method show => view($this->detail('abc'))
method show2 => view($this->view(5))
Just like with methods, parentheses are required when you provide arguments.
5.3: Single line and multiline
-
Singleline view
view: <p>test</p> -
Multiline view: the block ends with an empty line
view: <p>Line 1</p> <p>Line 2</p> view nextView: <p>Etc</p>
5.4: Shorthand HTML
Compact HTML shorthand is automatically converted:
# shorthand
<p#id.class1.class2/>
# equivalent
<p id="id" class="class1 class2"></p>
- A trailing slash on a tag ensures that the closing tag is automatically placed.
5.5: Variables and expressions
5.5.1 Direct variables and single properties
You can write directly in the view:
view($name):
<p>Hello $name, it is now $this->time.</p>
Direct allowed: regular variables and single property access (like
$this->time). No chained access or function/method calls directly in the HTML.
5.5.2 Functions, methods, chained or complex expressions
Use expression markers:
view($x = 1):
<p>{{ $this->call('test') }}</p>
<p>{( $x > 1 ? 'Multiple' : 'Single' )}</p>
<p>{{ $this->members->all }}</p>
{{ … }}and{( … )}are for evaluation of functions/methods/complex or chained expressions.- Use these markers in text and in attribute values (see 5.6).
(We intentionally do not impose any extra semantics on these two forms; both are intended for inline expressions.)
5.6: Attribute values
Attribute values may be without quotes if they do not contain spaces, @ or variables:
view:
<p title=correct>Correct answer</p>
<a href=/test1 data-value="$this->value">Link</a>
<a href=/test2 data-value="{{ $this->compute('value') }}">Link</a>
- With variables or expressions ⇒ use quotes (or an expression marker within quotes).
5.7: Statements
Use control-flow via tags in the HTML:
view:
<p>List:</p>
<foreach $this->list AS $key => $value>
<p>Item: $key</p>
<if $value > 1>
<p>High value: $value</p>
<elseif $value === 1>
<p>Exactly 1: $value</p>
<else>
<p>Other value: $value</p>
</if>
</foreach>
foreachopens with<foreach …>and closes with</foreach>.if/elseif/elseclose together with</if>.
5.8: Render view
A view is rendered with view($this->Name) (or the anonymous view) as a call. This call sends the output and terminates the script. The same applies to apply().
route GET home => $this->home
method home => view($this->home)
view home:
<h1>$this->title</h1>
Do not do:
# ❌ error – "gluing" views together does not exist; the first call terminates.
method dashboard {
view($this->header)
view($this->content)
}
If you want multiple pieces of output: create one view that combines them, or build it up in the view itself.
5.9: Best practices
- Keep views small and clear; split large parts up.
- Use direct variables/single properties in the HTML; anything more complex goes between
{{ … }}or{( … )}. - End multiline views with a blank line; use singleline where possible.
- Use shorthand for compact markup, but keep it readable.
- Remember that
view()andapply()are terminating: no "paste rendering".
6: CSS
Phlo uses a compact, semicolon-free CSS syntax within <style> blocks.
You write rules with colons as separators:
selector: property: value- nested:
A: B: property: value→ output:A B { property: value; }
Rules:
- One rule = one declaration. No semicolons in the source; the engine adds them.
- Colon
:separates chain levels and property from value. - Backslash
\in nestings “glues” the next selector part to the parent. That next part can be a pseudo (:…), attribute selector ([…]), etc. @media (…)can be inside a selector block; Phlo hoists this to the right place while keeping the current selector.
6.1: `<style>` block
<style>
html { height: 100dvh; }
body {
background: #947b6c;
font-family: Sans-serif;
}
body p { line-height: 2em; }
</style>6.2: Chains & groups
Chain with double colons; group with comma — the full context is applied per item.
<style>
body: h1, p: \:first-letter: color: green
</style>
- Context:
body - Targets:
h1andp(with glued:first-letter) - Backslash before
:first-letterglues that part to the preceding selector within the chain.
Output:
body h1:first-letter,
body p:first-letter { color: green; }6.3: Media queries in selector
You can write @media (…) inside the selector block; Phlo moves it to the right place and retains the selector context:
<style>
h1 {
color: white
@media (max-width: 768px): color: black
}
</style>
Output:
h1 { color: white; }
@media (max-width: 768px){
h1 { color: black; }
}6.4: Variables
Phlo supports CSS variables via $names.
You can define variables in :root, or at any other level — but :root is common for global theming.
<style>
:root {
$background: #0d0d0d
$surface: #1a1a1a
$text: #ffffff
$accent: #ff4a00
}
body {
background: $background
color: $text
}
button {
background: $accent
color: $text
}
</style>
Output
:root {
--background: #0d0d0d;
--surface: #1a1a1a;
--text: #ffffff;
--accent: #ff4a00;
}
body {
background: var(--background);
color: var(--text);
}
button {
background: var(--accent);
color: var(--text);
}
👉 Phlo automatically converts $variables to --custom-properties and uses var(--...) when calling.
You can reuse variables anywhere — even within media queries and nested selectors.
6.5: Dynamic variables
Phlo's frontend engine contains the library DOM/CSS.var, which allows you to access and modify defined $variables in CSS directly from JavaScript, via the global app.var object.
Each $variable in your CSS is automatically available under app.var.<name>.
Example
<style>
:root {
$background: #0d0d0d
$text: #ffffff
}
</style>
<script>
app.var.background = '#000000'
const textColor = app.var.text
</script>
app.var.background = '#000000'→ live updates the value of--backgroundin the DOM, without rebuild or reload.const textColor = app.var.text→ retrieves the current value.
👉 These adjustments work real-time in the browser and directly affect all elements that use the variable.
You can use this for, among other things:
- Theme switches (dark/light mode)
- Dynamically adjusting accent colors based on user input
- Toggling interactive UIs without separate CSS classes
Functionality
- The CSS engine converts
$backgroundto--background. - The frontend engine reads/writes this via
document.documentElement.style. app.varprovides a simple proxy object so you can work as if they are regular JS properties.
6.6: Complete example
body {
background: #947b6c;
font-family: Sans-serif;
}
body h1:first-letter,
body p:first-letter {
color: green;
}
body p {
line-height: 2em;
}
h1 {
color: white;
}
html {
height: 100dvh;
}
p {
color: navy;
}
p:last-child {
color: yellow;
}
@media (max-width: 768px){
h1 {
color: black;
}
}6.7: Best practices
- Use
$variablesfor colors, spacing, and fonts — this makes theming and dark/light modes easy. - Define global theme variables in
:root. - Use selector chains and grouping for compact, readable code.
- Just place
@mediain the block; Phlo will neatly position them in the right place. - Use
\in nestings to attach pseudo-classes or attributes to the parent selector. - No semicolons in your code; Phlo ensures correct CSS output.
7: ORM
Phlo includes a powerful built-in ORM that allows you to define database tables as classes.
Models can be quickly defined via columns or extensively via a declarative schema.
Records are treated as instances, with support for props, methods, views, relationships, and multiple database engines.
7.1: Basic principles
An ORM model is a .phlo file with:
@ class:name of the model (and table)@ extends: modelstatic tableandcolumnsorschema- Optional: relationships (
parent,child,many) - Props, methods, and views work per record-instance
Example:
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static columns = 'id,name,email,active,created'7.2: Defining models
7.2.1 Plate with columns (fast and light)
Use columns for simple tables:
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static order = 'changed DESC'
static columns = 'id,user,destination,costs,valid,weight,shipped,created,changed'
static objParents = ['user' => 'user']
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static order = 'changed DESC'
static columns = 'id,name,email,level,active,created,changed'
static objChildren = ['shipments' => 'shipment']
7.2.2 With schema and field(...) (rich and declarative)
With schema you define fields, relationships, and UI all at once:
@ class: shipment
@ extends: model
view: $this->destination ($this->user)
static table = 'shipments'
static schema => arr (
id: field (type: 'token', length: 4, title: 'ID'),
destination: field (type: 'text', required: true, search: true),
user: field (type: 'parent', obj: 'user', required: true),
costs: field (type: 'price', prefix: '€ '),
valid: field (type: 'bool'),
attachments: field (type: 'child', obj: 'attachment', list: true),
)
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static schema => arr (
id: field (type: 'token'),
name: field (type: 'text', search: true, required: true),
email: field (type: 'email', required: true),
shipments: field (type: 'child', obj: 'shipment'),
groups: field (type: 'many', obj: 'group', table: 'user_groups'),
)
schemais especially powerful in combination with PhloCMS, but also works standalone.
7.3: CRUD
# Retrieve
$user = user::record(id: 1)
$list = shipment::records(order: 'created DESC')
# Create
$shipment = shipment::create(destination: 'Paris', user: 1)
# Edit & save
$shipment->destination = 'Lyon'
$shipment->objSave
# Delete
shipment::delete('id=?', $shipment->id)
record(...)→ single record (or null)records(...)→ array of records (class instances)create(...)→ insert + instant retrieveobjSave→ save instance (insert/update depending on id)delete(...)→ static delete with SQL-where
7.4: Relational navigation
Relationships are available via properties:
| Type | Declaration | Usage |
|---|---|---|
| parent | type: parent |
$shipment->user |
| child | type: child |
$user->shipments |
| many | type: many |
$user->groups |
Many-to-many
type: many uses a pivot table:
groups: field (
type: 'many',
obj: 'group',
table: 'user_groups',
)
Navigation:
$user = user::record(id: 1)
foreach ($user->groups as $group)
echo $group->title
Relationships are batch-loaded for performance. No cross-DB joins occur; each class loads from its own engine.
7.5: Instance dynamics
Each record is a real instance of your model class.
You can use props, methods, and views to add virtual fields, calculations, or representations:
@ class: shipment
@ extends: model
prop summary => $this->destination.' ('.$this->user.')'
method tax => $this->costs * 0.21
view:
<p>$this->summary</p>
<p>{( $this->tax )}</p>
Usage:
$shipment = shipment::record(id: 'AB12')
echo $shipment->summary
echo $shipment # uses view as string
Props and methods always operate on the record instance, not statically.
7.6: Filtering en queries
All query methods accept named arguments and SQL-like filters:
shipment::records(destination: 'Paris')
shipment::records(where: 'valid=1 AND weight>10')
shipment::pair(columns: 'id,destination')
Supported: where, order, group, joins, caching, and schema-aware columns.
7.7: Caching and performance
The ORM uses internal buffers (objRecords, objLoaded) for relational lookups and optional APCu caching via:
static objCache = true # 1 day
# or
static objCache = 600 # 10 minutes
Records and relationships are loaded in batches. Use records() for bulk selections instead of record() in loops.
7.8: Multiple engines
Phlo supports multiple backends via prop DB. By default, it is %MySQL, but you can set any engine per model.
SQLite
prop DB => %SQLite(data.'users.db')
@ class: notes
@ extends: model
prop DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
PostgreSQL
prop DB => %PostgreSQL
@ class: invoices
@ extends: model
prop DB => %PostgreSQL
static table = 'invoices'
static columns = 'id,customer_id,total,created'
Tables on different engines can be combined in relationships; each class retrieves its own data.
/data/creds.ini
For engines like MySQL and PostgreSQL, place your credentials in:
/data/creds.ini
[mysql]
host = localhost
database = db_name
user = db_user
password = db_password
[postgresql]
host = localhost
database = my_pg_db
user = pg_user
password = pg_pass
Phlo automatically loads these via %creds->....
7.9: Overview of functions
| Function / Property | Type | Description |
|---|---|---|
record(...) |
static | Retrieves 1 record (or null) |
records(...) |
static | Retrieves multiple records (array) |
create(...) |
static | Insert + retrieve |
objSave |
instance | Insert or update |
delete(where, …) |
static | Deletion |
pair, item, column |
static | Quick query helpers |
objParents / schema: parent |
declarative | Parent relationships |
objChildren / schema: child |
declarative | Child relationships |
schema: many |
declarative | Many-to-many |
objCache |
static | Optional APCu caching |
prop DB |
static | Per-model engine |
7.10: Best practices
- Use
columnsfor quick, simple models. - Use
schemafor rich definitions and CMS integration. - Define a
viewfor string representations. - Use props for virtual fields.
objSaveinstead ofsave.- Use
records()for bulk loads. - Separate models by engine with
prop DB. - Use credentials in
/data/creds.ini. - Keep models declarative; logic in props/methods.
8: Instance Management
Phlo uses its own instance manager to efficiently and predictably initialize and reuse objects. This system determines when controller code is executed, how instances are stored, and how circular references are prevented.
8.1: Basic explanation
When you define a .phlo file, it is converted into a class during the build phase.
Each call to an object via %name goes through the instance manager (phlo() in /phlo/phlo.php).
Example:
prop title = 'Welcome'
route GET home => $this->main
method main => view($this->home)
view home:
<h1>$this->title</h1>
When the route /home is called:
- The instance manager checks if an instance of this class already exists.
- If not, it is created and stored.
- After creation, the controller code is executed (see §8.2).
- Then the requested method is called.
8.2: Controller code
All code in a .phlo file that does not fall under route, prop, static, method, function, view, <style> or <script> is controller code.
This code is executed after instantiation, as soon as the instance is fully created.
Example:
prop ready = false
%session->start() # controller code (top-level)
$this->ready = true
- This code runs once during the first creation of the instance.
- The difference with
__constructis that the instance fully exists at the moment controller code is executed → this prevents circular references and incomplete objects.
8.3: The role of `__handle()`
Phlo generates a special __handle() method for each class. This is called by the instance manager when an instance is requested via %name.
__handle():
- executes the controller code if it hasn't been done yet;
- ensures that props and statics are correctly available;
- handles caching of the instance for subsequent calls.
You do not need to call or override __handle() yourself — it is part of the generated class and the instance manager.
8.4: Lazy initialization
Because controller code is executed only after construction, instances can refer to each other without unwanted recursive creation occurring.
Example:
# file a.phlo
prop message = 'A ready'
# file b.phlo
prop message = 'B ready'
# file main.phlo
route GET test => $this->show
method show {
dx(%a->message, %b->message)
}
%aand%bare created lazily.- The controller code in both files runs once their instance is fully created.
- You can refer to each other without issues, as the instance already exists before the controller code runs.
8.5: Best practices
- Use controller code for initial setup, not for request-dependent logic.
- Place controller code at the top or directly under props for clarity.
- Avoid side effects in
__construct; use controller code instead of custom constructors. - Let instances lazy initialize themselves via
%nameinstead of manual creation. - Use controller code consciously to resolve circular references.
9: Tooling & CLI
Phlo has a very lightweight toolchain. There is no external compiler or CLI framework needed: everything runs on the Phlo engine itself in /phlo/. During requests, Phlo automatically takes care of the building and updating of the generated PHP, JS, and CSS files. You can manually start this process for CI/CD or pre-release builds, but in normal setups, that is not necessary.
9.1: Build process
Phlo transpiles .phlo files to PHP, JS, and CSS. This happens:
- Automatically on changes during requests (JIT build).
- Optionally manually, by calling
phlo_build()(e.g., from a script or CI).
The entry points:
/www/app.php– central PHP entry point/www/app.js– automatically generated frontend bundle/www/app.css– automatically generated CSS bundle
For each request, Phlo checks:
- if there are changes in the source files,
- if the generated files are up-to-date,
- and performs an incremental build if necessary.
9.2: CLI
Phlo does not need an external CLI framework; you can just use php.
For special situations (prebuilds, CI, staging) you can build manually by:
php -r "require 'phlo/build.php'; phlo_build();"
or from your own PHP script:
<?php
require 'phlo/build.php';
phlo_build();
This goes through the same build process as at runtime, but outside the request context.
So there is no
phloCLI command or package. You just use PHP itself.
9.3: Debug mode
Debug mode is set in data/app.json via:
{
"debug": true
}
In debug mode:
- Errors and backtraces are displayed in the browser.
- Changes to
.phlofiles are rebuilt immediately. - The debug overlay of the frontend is active (if configured).
9.4: Deployment
For production:
- Set
"debug": falseindata/app.json. - Run a manual
phlo_build()so that all files are pre-transpiled. - Put the
/www/folder online (includingapp.php,app.js,app.css). - Ensure that the
/php/folder with generated backend code is moved as well.
Phlo has no dependencies on npm, webpack, or other toolchains. All tooling is built-in and runs directly under PHP.
9.5: Best practices
- Let the build process run as much as possible automatically; manual building only for release or CI.
- Do not change anything manually in
/www/app.jsor/www/app.css— these are always overwritten. - Use
php -r "require ...; phlo_build();"for precompilation in CI. - Set
"debug": falsefor production to minimize performance overhead. - Keep the
/phlo/folder up-to-date in your deployment, as it contains the engine.
✅ Correct explanation of the build process (JIT + manual via phlo_build())
✅ No fake CLI commands
✅ Debug mode as in app.json
✅ Deployment as Phlo actually does
✅ Correct handling of /www/app.js and /www/app.css (never change manually)
10: Translations
Phlo has built-in support for multilingualism and dynamic translation. The engine includes features and libraries that allow you to translate text inline, apply language switches, and load async translations without interrupting the user experience.
10.1: Language helpers
Phlo offers short helper functions for the most commonly used languages, which you can directly use in views and code:
nl()– Dutchen()– Englishde()– Germanfr()– Frenches()– Spanish
Example in a view:
view:
<p>{( nl('Hallo wereld') )}</p>
<p>{( en('Hello world') )}</p>
The language helpers:
- directly return the string in the respective language context,
- are mainly used for multilingual content blocks and static texts.
10.2: Other use
For dynamic translations, Phlo offers two core functions: translate() and translation().
translate()
- Synchronous, returns the translation of a string immediately in the current language.
- Convenient for texts that are already translated in the system.
<p>{( translate('welcome_message') )}</p>
translation()
- Asynchronous, intended for dynamic or not-yet-loaded translations.
- First, the source text is displayed, then the translation is loaded and updated in the UI.
- The translation is returned in the current active language code of the app.
<p>{( translation('dynamic_intro_text') )}</p>
- This prevents delays in rendering pages.
- The frontend engine performs a translation request in the background and adjusts the text as soon as the translation is available.
10.3: Dynamic language selection
The active language can be dynamically set and changed via the frontend. When the user switches languages, the UI is automatically retranslated without a reload.
Typical process:
- App starts with a default language (e.g.
nl). - The user selects a different language.
- The frontend engine switches the language code.
- Texts loaded via
translation()are retranslated as soon as the new language is available. - Texts via helpers like
nl()oren()continue to show their fixed value.
10.4: Best practices
- Use
translate()for texts that you manage locally or statically. - Use
translation()for texts that come from external sources or are loaded asynchronously. - Use language helpers (
nl(),en(), …) for static content blocks or fixed view texts. - Keep your translation keys consistent and structured (e.g.
page.home.introinstead of loose strings). - Centralize the language switch in the app so that all UI elements change consistently.
- Combine translation functions with Phlo’s frontend engine for a smooth language switch without reloads.
11: Advanced
Phlo is designed as a modular system. You can use parts of the engine separately, combine them with existing projects, or expand with your own functionality, without having to modify the core.
11.1: Modular use
Phlo can be used both as a complete framework and in parts.
For example:
- Use only the frontend engine, without the backend.
- Use only the backend transpiler, for example as a class generator or asset bundler.
- Use only specific libraries (
/phlo/libs) in an existing PHP project. - Use Phlo as a router + view engine, but keep your own database layer.
Example: use only the frontend in a static site
<script src="/app.js" defer></script>
and then call Phlo-frontend functions like app.apply() and app.state without a .phlo backend.
Or: integrate Phlo into an existing PHP application and add just a few .phlo files in sources.
Phlo will then transpile them to /php/, after which you can use the generated classes directly.
11.2: Integration
You can integrate Phlo into an existing codebase by:
- Placing the Phlo engine in a subdirectory, e.g.
/vendor/phlo/. - Adding
app.phpas a central router for all non-existing routes. - Updating the
build.sourcesfield inapp.jsonwith the paths of your existing code, alongside the Phlo folders.
Example data/app.json:
{
"id": "LegacyApp",
"version": ".1",
"host": "localhost",
"dashboard": "phlo",
"debug": true,
"build": {
"libs": ["session", "json"],
"sources": [
"%app/",
"/legacy/phlo/"
]
}
}
This allows you to keep existing PHP code while still adding .phlo files. Phlo transpiles them and adds them to your project without needing to overhaul your entire structure.
11.3: Best practices
- Keep custom commands small and reusable.
- Add integration gradually; for example, start with just routing or views.
- Place separate modules in
/phlo/libsif you use them more often, instead of baking them separately. - Use
sourcesto combine multiple codebases without duplication. - Test custom apply commands thoroughly in conjunction with Phlo's update mechanism.
12: Attachments
The attachments contain ready-made examples, project templates, and extra showcases that demonstrate how Phlo is applied in practice. This section is intended as a reference and source of inspiration — not as primary documentation.
12.1: Code examples
A collection of small, well-readable .phlo fragments that illustrate commonly used patterns:
Basic route with view
prop title = 'Welcome'
route GET home => $this->main
method main => view($this->home)
view home:
<h1>$this->title</h1>
Route with variable
route GET user $id => $this->show($id)
method show($id) {
$user = %users->record(id: $id)
view($this->profile, $user)
}
view profile($user):
<h1>$user->name</h1>
Async translation
view:
<p>{( translation('welcome_text') )}</p>12.2: Project template
A minimal project structure with all required components:
/www/
app.php ← central entry point
app.js ← generated frontend bundle
app.css ← generated CSS
/data/
app.json ← required configuration
/phlo/ ← Phlo engine
/php/ ← transpiled backend code (automatically)
/app/ ← your Phlo source files (*.phlo)
This is the recommended basic structure.
Extra sources can be defined in data/app.json if you want to combine multiple source paths.
12.3: CSS Showcase
A visual example of Phlo's compact CSS syntax:
Input
<style>
html: height: 100dvh
body {
background: #947b6c
font-family: Sans-serif
p: line-height: 2em
}
body: h1, p: \:first-letter: color: green
h1 {
color: white
@media (max-width: 768px): color: black
}
p {
color: navy
\:last-child: color: yellow
}
</style>
Output
body {
background: #947b6c;
font-family: Sans-serif;
}
body h1:first-letter,
body p:first-letter {
color: green;
}
body p {
line-height: 2em;
}
h1 {
color: white;
}
html {
height: 100dvh;
}
p {
color: navy;
}
p:last-child {
color: yellow;
}
@media (max-width: 768px){
h1 {
color: black;
}
}12.4: ORM Models
Example of a typical ORM type definition in Phlo:
type users {
static columns = null
field(id, type: number, auto: true)
field(name, type: text)
field(email, type: text)
}
route GET users => $this->overview
method overview {
foreach (%users->records(order: 'name') as $u)
dx($u->id, $u->name)
}
Retrieve a single record:
$user = %users->record(id: 1)
dx($user->name)12.5: Further sources
- phloCMS — complete CMS built on Phlo
- phloWS — Web Services layer
- phloWA — WhatsApp Gateway module
- demo.zip — includes among others a showcase of CSS, views, and routing