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:

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'),
)

schema is 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)

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, fromto
delete(...) volledige oude waarden, per aangetast record

Instelling:

  1. Voeg security/audit toe aan de resources in data/app.json.
  2. 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:

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

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.