4: Data
The poll needs storage. Phlo's ORM defines a table as a class, and for a tutorial app the JSON file driver is perfect: no database server, no credentials, one JSON file per table. In this chapter you define the model, seed it with options, and render real records.
4.1: The model file
Create type.poll.phlo:
@ extends: model
static table = 'poll'
static order = 'votes DESC'
static DB => %JSONDB(data.'poll.json')
static columns = 'id,option,votes'
static total => array_sum(array_map(fn($row) => (int)$row->votes, static::records()))
Line by line: @ extends: model makes this a model class. The file name type.poll.phlo gives the class its name, type_poll (dots become underscores). table and columns describe the table: an id, the option text (string), and a vote count (int). static DB => %JSONDB(data.'poll.json') points the model at the JSON driver; %JSONDB(...) is the instance shorthand for phlo('JSONDB', ...), and data is the app's data path constant, so the table lives in data/poll.json. The computed static total sums all votes; you need it for percentages.
One JSONDB trait to remember: records come back as plain data objects, so keep computed values (like total) on the class as statics or on the controller, and write changes through the static model methods rather than on the record instance.
4.2: Switch on the database resources
Resources are opt-in. Open data/app.json and list what the model layer needs:
{
"resources": [
"DB/DB",
"DB/model",
"DB/JSONDB",
"DB/JSON.result"
]
}
DB/model is the ORM, DB/JSONDB the file driver, DB/JSON.result its result wrapper, and DB/DB the shared base. When you edit data/app.json by hand you list requirements yourself; the Phlo Control Center resolves them for you, the text editor does not.
Rebuild and lint:
php www/app.php build::run
php www/app.php build::lint
build::run prints the new compiled files (+type.poll.php, +model.php, +JSONDB.php, ...), and build::lint returns [].
4.3: Seed the options
The poll should create its own options on first run. In poll.phlo, replace the options prop and the home method with:
route GET poll => $this->home
method home {
$this->seed
view($this, 'Phlo Poll')
}
method seed {
if (type_poll::records()) return
foreach (['Phlo', 'Next.js', 'Laravel', 'Rails'] AS $option){
type_poll::create(option: $option, votes: 0)
}
}
type_poll::records() fetches all records; if any exist, seeding is skipped. type_poll::create(...) inserts a record with named arguments. Note $this->seed without parentheses: methods without arguments are called like properties.
Reload http://localhost/poll once, then look at the storage:
docker run --rm -v $(pwd)/app:/app ghcr.io/q-ainl/phlo cat /app/data/poll.json
[
{
"option": "Phlo",
"votes": 0,
"id": 1
},
{
"option": "Next.js",
"votes": 0,
"id": 2
}
]
(Plus Laravel and Rails.) The table is a readable JSON file; ids were assigned automatically.
4.4: Render the records
Now replace the static buttons with records, and add a results section. The full poll.phlo view section:
prop question = 'Which stack wins?'
method share($votes) => ($total = type_poll::total()) ? round($votes / $total * 100) : 0
view:
<main#app.poll>
<h1>$this->question</h1>
{{ $this->results }}
{{ $this->choices }}
</main>
view results:
<section#results.card>
<foreach type_poll::records() AS $option>
<div.option>
<span.name>$option->option</span>
<span.votes>$option->votes</span>
<div.track>
<div.bar style="width: {{ $this->share($option->votes) }}%"></div>
</div>
</div>
</foreach>
</section>
view choices:
<section.card>
<foreach type_poll::records() AS $option>
<button>$option->option</button>
</foreach>
</section>
The share method turns a vote count into a percentage; {{ ... }} calls it inside the style attribute. The #results id matters later: it is the element you will update in place. Add the bar styles to poll.style.phlo, inside the <style ns=app> block:
.option {
display: grid
grid-template-columns: 1fr auto
gap: 4px 12px
margin-bottom: 14px
}
.track {
grid-column: 1 / -1
height: 8px
border-radius: 4px
background: $border
}
.bar {
height: 100%
border-radius: 4px
background: $primary
transition: width .4s
}
.votes: color: $muted
Reload http://localhost/poll: four options, vote counts at 0, empty bars. Time to let people vote.