8: ORM
Phlo wordt geleverd met een krachtige ingebouwde ORM waarmee je databasetabellen kunt definiëren als klassen. Modellen kunnen snel worden gedefinieerd via columns of volledig via een declaratieve schema. Records worden behandeld als instances, met ondersteuning voor props, methods, views, relaties en meerdere database-engines.
8.1: Basisprincipes
Een ORM-model is een .phlo-bestand met:
@ class:de naam van het model (en tabel)@ extends: modelstatic tableencolumnsofschema- Optioneel: relaties (
parent,child,many) - Props, methods en views werken per record instance
Voorbeeld:
@ class: user
@ extends: model
view => $this->name
static table = 'users'
static columns = 'id,name,email,active,created'8.2: Modellen definiëren
7.2.1 Plat met columns (snel en lichtgewicht)
Gebruik columns voor eenvoudige tabellen:
@ 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 Met schema en field(...) (rijk en declaratief)
Met schema definieer je velden, relaties en UI op één plek:
@ 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 vooral krachtig in combinatie met PhloCMS, maar werkt ook zelfstandig.
8.3: CRUD
Ophalen:
$user = user::record(id: 1)
$list = shipment::records(order: 'created DESC')
Aanmaken:
$shipment = shipment::create(destination: 'Paris', user: 1)
Bewerken en opslaan:
$shipment->destination = 'Lyon'
$shipment->objSave
Verwijderen:
shipment::delete('id=?', $shipment->id)
record(...)→ enkele record (of null)records(...)→ array van records (klasse-instanties)create(...)→ invoegen + directe ophalenobjSave→ sla de instantie op (invoegen/bijwerken afhankelijk van id)delete(...)→ statische verwijdering met een SQL where-clausule
8.4: Relationele navigatie
Relaties zijn beschikbaar als eigenschappen:
| Type | Declaratie | Gebruik |
|---|---|---|
| parent | type: parent |
$shipment->user |
| child | type: child |
$user->shipments |
| many | type: many |
$user->groups |
Veel-op-veel
type: many gebruikt een pivot-tabel:
groups: field (
type: 'many',
obj: 'group',
table: 'user_groups',
)
Navigatie:
$user = user::record(id: 1)
foreach ($user->groups as $group)
echo $group->title
Relaties worden batch-geladen voor prestaties. Er zijn geen cross-DB joins; elke klasse laadt vanuit zijn eigen engine.
8.5: Instantiedynamiek
Elk record is een echte instantie van je modelklasse. Je kunt props, methods en views gebruiken om virtuele velden, berekeningen of representaties toe te voegen:
@ 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>
Gebruik:
$shipment = shipment::record(id: 'AB12')
echo $shipment->summary
echo $shipment
Het gebruik van een record als een string roept de weergave ervan op.
Props en methods werken altijd op de record instance, nooit statisch.
8.6: Filteren en queries
Alle query methoden accepteren benoemde argumenten en SQL-achtige filters:
shipment::records(destination: 'Paris')
shipment::records(where: 'valid=1 AND weight>10')
shipment::pair(columns: 'id,destination')
Ondersteund: where, order, group, joins, caching en schema-bewuste kolommen.
8.7: Caching en prestaties
De ORM gebruikt interne buffers (objRecords, objLoaded) voor relationele opzoekingen en optionele APCu caching via:
static objCache = true
Of een aantal seconden:
static objCache = 600
Records en relaties worden in batches geladen. Gebruik records() voor bulkselecties in plaats van record() in lussen.
8.8: Meerdere engines
Phlo is database-agnostisch: een model bepaalt zijn engine vanuit static DB, en er is geen impliciete standaard. Stel de engine één keer in voor de hele app door deze in de modelbasis te injecteren vanuit app.phlo:
static %model.DB => %MySQL
Overschrijf het voor een enkel model met zijn eigen static DB.
SQLite
@ class: notes
@ extends: model
static DB => %SQLite(data.'notes.db')
static table = 'notes'
static columns = 'id,title,body'
PostgreSQL
@ class: invoices
@ extends: model
static DB => %PostgreSQL
static table = 'invoices'
static columns = 'id,customer_id,total,created'
Tabellen op verschillende engines kunnen worden gecombineerd in relaties; elke klasse haalt zijn eigen gegevens op.
/data/creds.ini
Voor engines zoals MySQL en PostgreSQL, plaats je inloggegevens 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 laadt deze automatisch via %creds->....
8.9: Opt-in functies: audit, validatie, aangepaste PK
Een @ extends: model biedt je CRUD + identity map + relaties direct uit de doos. Drie extra opt-ins zijn per model ingeschakeld met een static vlag.
8.9.1 Auditlog
static objAudit = true
Vanaf dat moment wordt elke create, objSave (update) en delete gelogd in een audit-tabel via de security/audit resource:
| Operatie | Wat wordt gelogd |
|---|---|
create(...) |
volledige nieuwe waarden |
objSave (update) |
diff: alleen gewijzigde velden, from → to |
delete(...) |
volledige oude waarden, per aangetast record |
Instelling:
- Voeg
security/audittoe aan de resources indata/app.json. - Importeer de schema SQL eenmaal:
mysql <database> < /opt/phlo/resources/security/audit.sql.
Gevoelige velden uitsluiten:
method afterCreate => %audit->log($this, 'create', [], (array)$this, exclude: ['password_hash'])
Schakel per omgeving, alleen dev:
static objAudit => debug
Of alleen release:
static objAudit => !debug
(debug is de runtime constante van phlo_app(debug: ...).)
De log lezen. De security/audit resource biedt ook een kleine query API, met het model als het eerste argument:
| Aanroep | Retourneert |
|---|---|
%audit->history($model, $recordId, $limit = 50) |
elke wijziging aan één record, nieuwste eerst |
%audit->byUser($model, $userId, $fromTs = 0, $limit = 100) |
elke wijziging die een gebruiker heeft gemaakt sinds $fromTs |
%audit->purge($model, $olderThanSeconds = 31536000) |
verwijder vermeldingen ouder dan de cutoff, voor behoud |
$rows = %audit->history(invoice, $invoiceId)
Elke audit_log rij bevat ts, user (de %session->user op het moment van de wijziging, of null), model, record_id, action, ip, en een changes JSON blob waarvan de structuur volgt op de actie: een create slaat de volledige nieuwe rij op, een update slaat alleen de gewijzigde velden op als {"col": {"from": ..., "to": ...}}, en een delete slaat de volledige oude rij op.
8.9.2 Validatie
static objValidate = true
Voor create() voert Phlo de bijbehorende objValidate($value) uit voor elk veld in static schema(). Bij fouten: create() retourneert null, met fouten beschikbaar via Class::objErrors():
if (!user::create($args)){
return apply(errors: user::objErrors())
}
Veldregels in schema():
static schema => arr (
email: field (type: 'email', required: true),
name: field (type: 'text', length: 100, required: true),
slug: field (type: 'text', pattern: '^[a-z0-9-]+$'),
status: field (type: 'text', enum: ['draft', 'sent', 'paid']),
)
Aangepaste veldvalidatie: overschrijf method objValidate($value) in je eigen veldsubklasse.
8.9.3 Aangepaste primaire sleutel
De standaard is id (auto-increment integer). Overschrijf het voor andere PK's:
static idColumn = 'sku'
static idType = 'string'
Effect:
- De identiteitskaart gebruikt
skuals zijn sleutel Class::record(sku: 'XYZ-123')(nietid:)- Met
create(): je levert zelf de PK-waarde aan (geen auto-increment) $record->idwerkt niet, gebruik$record->sku
8.9.4 Combineren
De drie opt-ins zijn onafhankelijk en kunnen worden gecombineerd:
@ class: giftcard
@ extends: model
static objAudit = true
static objValidate = true
static idColumn = 'sku'
static idType = 'string'
Elke functie is standaard uit (false, 'id', 'int'). Een model zonder opt-ins blijft een gewoon model.
8.10: Functiebeschrijving
| Functie / Eigenschap | Type | Beschrijving |
|---|---|---|
record(...) |
static | Haalt 1 record (of null) op |
records(...) |
static | Haalt meerdere records (array) op |
create(...) |
static | Invoegen + ophalen |
objSave |
instance | Invoegen of bijwerken |
delete(where, …) |
static | Verwijderen |
pair, item, column |
static | Snelle query-hulpmiddelen |
objParents / schema: parent |
declarative | Ouderrelaties |
objChildren / schema: child |
declarative | Kindrelaties |
schema: many |
declarative | Veel-op-veel |
objCache |
static | Optionele APCu-caching |
static DB |
static | Per-model engine |
8.11: Best practices
- Gebruik
columnsvoor snelle, eenvoudige modellen. - Gebruik
schemavoor rijke definities en CMS-integratie. - Definieer een
viewvoor stringrepresentaties. - Gebruik props voor virtuele velden.
objSavein plaats vansave.- Gebruik
records()voor bulklaadbeurten. - Scheid modellen per engine met
static DB; stel de app-brede standaard in metstatic %model.DB. - Houd inloggegevens in
/data/creds.ini. - Houd modellen declaratief; plaats logica in props/methods.