ORM und Query‑Builder
Das ORM und den Query-Builder für Modelle, Relationen, Multi‑Tenancy, Hooks und direkten Datenbankzugriff erlernen.
Das ORM und den Query-Builder für Modelle, Relationen, Multi‑Tenancy, Hooks und direkten Datenbankzugriff erlernen.
Booknetic verwendet ein benutzerdefiniertes ORM im ActiveRecord-Stil, das auf WordPress $wpdb aufbaut. Es bietet eine praktische und ausdrucksstarke Möglichkeit, mit Datenbankeinträgen zu arbeiten, indem Modelle, ein verkettbarer Query Builder, Collection-Objekte, Modellbeziehungen, globale Scopes, Lifecycle-Hooks und automatische Multi-Tenant-Isolierung verwendet werden.
Für die Entwicklung von Addons und internen Funktionen ist dieses ORM die zentrale Datenbankschicht, die in ganz Booknetic verwendet wird. Es ermöglicht dir, lesbare modellbasierte Abfragen zu schreiben, anstatt für gängige Operationen manuell SQL zu erstellen.
Das ORM basiert auf vier Kernklassen:
| Klasse | Namespace | Rolle |
|---|---|---|
Model | BookneticApp\Providers\DB\Model | Basisklasse für alle Modelle. Definiert Tabelle, Beziehungen, Scopes und Lifecycle-Hooks. |
QueryBuilder | BookneticApp\Providers\DB\QueryBuilder | Verkettbarer Query Builder. Behandelt WHERE, JOIN, ORDER BY und ähnliche Abfrageoperationen. |
Collection | BookneticApp\Providers\DB\Collection | Kapselt eine einzelne Datenbankzeile. Bietet Eigenschaftszugriff, Array-Zugriff und das Laden von Beziehungen. |
DB | BookneticApp\Providers\DB\DB | Low-Level-Datenbank-Wrapper rund um WordPress $wpdb. Behandelt Raw-Queries und die Auflösung von Tabellennamen. |
Der Datenfluss sieht so aus:
Model::query() → QueryBuilder → DB (raw SQL) → Collection (result)Modelle repräsentieren Datenbanktabellen und dienen als Einstiegspunkt für die meisten ORM-Operationen.
Jede Datenbanktabelle wird durch eine Modellklasse innerhalb von app/Models/ repräsentiert. Ein Modell erweitert BookneticApp\Providers\DB\Model:
<?php
namespace BookneticApp\Models;
use BookneticApp\Providers\DB\Model;
use BookneticApp\Providers\DB\MultiTenant;
class Customer extends Model
{
use MultiTenant;
public static $relations = [
'category' => [CustomerCategory::class, 'id', 'category_id']
];
}Das reicht aus, damit das Modell funktionsfähig ist. Eine explizite Definition des Tabellennamens ist nicht erforderlich, es sei denn, du möchtest den Standardwert überschreiben.
Tabellennamen werden automatisch aus dem Klassennamen nach dieser Regel generiert:
PascalCase → snake_case + Plural
| Modellklasse | Generierte Tabelle | Vollständige DB-Tabelle |
|---|---|---|
Customer | customers | wp_bkntc_customers |
Appointment | appointments | wp_bkntc_appointments |
ServiceExtra | service_extras | wp_bkntc_service_extras |
AppointmentPrice | appointment_prices | wp_bkntc_appointment_prices |
Alle Booknetic-ORM-Tabellen verwenden das Präfix {wp_base_prefix}bkntc_, zum Beispiel wp_bkntc_.
Falls nötig, kannst du den generierten Tabellennamen überschreiben:
class MyModel extends Model
{
protected static $tableName = 'custom_table_name';
}Die folgenden integrierten Modelle werden häufig verwendet:
| Modell | Tabelle | Multi-Tenant | Beschreibung |
|---|---|---|---|
Appointment | appointments | Ja | Buchungen |
AppointmentExtra | appointment_extras | Nein | Extras, die an Termine angehängt sind |
AppointmentPrice | appointment_prices | Nein | Preisaufschlüsselung pro Termin |
Customer | customers | Ja | Kundendatensätze |
CustomerCategory | customer_categories | Ja | Kundengruppierung |
Service | services | Ja | Verfügbare Services |
ServiceCategory | service_categories | Ja | Service-Gruppierung |
ServiceExtra | service_extras | Ja | Zusatz-Extras für Services |
ServiceStaff | service_staffs | Nein | Zuordnung Service–Mitarbeiter |
Staff | staffs | Ja | Mitarbeiter |
Location | locations | Ja | Geschäftsstandorte |
Holiday | holidays | Ja | Freie Tage |
SpecialDay | special_days | Ja | Besondere Zeitplan-Überschreibungen |
Timesheet | timesheets | Ja | Arbeitszeiten |
Workflow | workflows | Ja | Automatisierungsregeln |
WorkflowLog | workflow_logs | Ja | Ausführungsprotokolle von Automatisierungen |
Appearance | appearances | Ja | Theme-Einstellungen des Buchungspanels |
Data | data | Nein | Key-Value-Metaspeicher |
Translation | translations | Nein | Mehrsprachige Inhalte |
Der Query Builder ist die zentrale Schnittstelle zum Abrufen, Filtern, Verknüpfen, Aktualisieren und Löschen von Datensätzen.
Jede Abfrage beginnt bei einem Modell:
// Option 1: Mit query()
$query = Customer::query();
// Option 2: Methoden direkt auf dem Modell aufrufen (erstellt automatisch QueryBuilder)
$customer = Customer::where('email', '[email protected]')->fetch();
// Option 3: Nach ID abrufen
$customer = Customer::get(5);// Gibt ein Collection-Objekt zurück oder null, wenn nichts gefunden wurde
$customer = Customer::query()
->where('email', '[email protected]')
->fetch();
echo $customer->first_name;
echo $customer->email;// Gibt ein Array von Collection-Objekten zurück
$customers = Customer::query()
->where('gender', 'male')
->orderBy(['first_name' => 'ASC'])
->fetchAll();
foreach ($customers as $customer) {
echo $customer->first_name . ' ' . $customer->last_name;
}// Gibt ein Array von assoziativen Arrays zurück (ohne Collection-Wrapper)
$rows = Customer::query()
->where('gender', 'male')
->fetchAllAsArray();$appointment = Appointment::get(42);
// Entspricht: Appointment::query()->where('id', 42)->fetch();// Gleichheit
Customer::where('email', '[email protected]')->fetch();
// Mit Operator
Appointment::where('starts_at', '>', '2025-01-01 00:00:00')->fetchAll();
// Mehrere Bedingungen (AND)
Appointment::query()
->where('status', 'approved')
->where('staff_id', 3)
->fetchAll();// Mehrere Bedingungen als Array übergeben
Appointment::where([
'status' => 'approved',
'staff_id' => 3
])->fetchAll();Customer::query()
->where('first_name', 'John')
->orWhere('first_name', 'Jane')
->fetchAll();// Kurzform für where('id', $value)
Customer::whereId(5)->fetch();// IS NULL
Appointment::query()->whereIsNull('recurring_id')->fetchAll();
// IS NOT NULL
Appointment::query()->whereIsNotNull('note')->fetchAll();
// Direkt mit where()
Appointment::where('tenant_id', 'is', null)->fetchAll();
Appointment::where('tenant_id', 'is not', null)->fetchAll();// Ein Array als Wert übergeben
Appointment::where('status', ['approved', 'pending'])->fetchAll();
// NOT IN
Appointment::where('status', 'not in', ['canceled', 'rejected'])->fetchAll();// Umschließt automatisch mit %...%
Customer::query()->like('first_name', 'John')->fetchAll();
// Erzeugt: WHERE first_name LIKE '%John%'
// OR LIKE
Customer::query()
->like('first_name', 'John')
->orLike('last_name', 'John')
->fetchAll();Für Spalten mit kommagetrennten Werten:
Service::query()->whereFindInSet('staff_ids', 5)->fetchAll();
// Erzeugt: WHERE FIND_IN_SET('5', staff_ids)Customer::query()
->where(function ($query) {
$query->where('first_name', 'John')
->orWhere('first_name', 'Jane');
})
->where('gender', 'male')
->fetchAll();
// Erzeugt: WHERE (first_name = 'John' OR first_name = 'Jane') AND gender = 'male'// Kunden abrufen, die mindestens einen Termin haben
$appointmentCustomers = Appointment::query()->select('customer_id', true);
Customer::query()
->where('id', 'in', $appointmentCustomers)
->fetchAll();
// Erzeugt: WHERE id IN (SELECT customer_id FROM wp_bkntc_appointments ...)// Bestimmte Spalten auswählen
Customer::query()
->select(['first_name', 'last_name', 'email'])
->fetchAll();
// Alle vorherigen Selects ersetzen
Customer::query()
->select(['first_name', 'email'], true)
->fetchAll();
// SubQuery als Spalte
$countQuery = Appointment::query()
->select(['COUNT(*)'], true)
->where('customer_id', DB::field('id', Customer::getTableName()));
Customer::query()
->selectSubQuery($countQuery, 'appointment_count')
->fetchAll();
// Jedes Ergebnis hat jetzt ->appointment_countAppointment::query()
->orderBy(['starts_at' => 'DESC'])
->fetchAll();
// Mehrere Sortierspalten
Appointment::query()
->orderBy(['status' => 'ASC', 'starts_at' => 'DESC'])
->fetchAll();Appointment::query()
->select(['status', 'COUNT(*) as count'])
->groupBy(['status'])
->fetchAll();$page = 2;
$perPage = 10;
$appointments = Appointment::query()
->orderBy(['starts_at' => 'DESC'])
->limit($perPage)
->offset(($page - 1) * $perPage)
->fetchAll();// Count
$count = Appointment::query()
->where('status', 'approved')
->count();
// Count mit GROUP BY
$count = Appointment::query()
->groupBy(['status'])
->countGroupBy();
// Sum
$total = Appointment::query()
->where('payment_status', 'paid')
->sum('paid_amount');$appointments = Appointment::query()
->leftJoin('customers', ['first_name', 'last_name', 'email'], 'customer_id', 'id')
->where('status', 'approved')
->fetchAll();
// Auf gejointen Spalten kann mit Tabellenpräfix zugegriffen werden:
foreach ($appointments as $apt) {
echo $apt->customers_first_name; // Spaltenalias: {table}_{column}
echo $apt->customers_email;
}Die Join-Methoden verwenden diese Signatur:
->leftJoin($joinTo, $selectFields, $field1, $field2, $unselectOldFields, $alias)
->rightJoin(...)
->innerJoin(...)| Parameter | Typ | Beschreibung |
|---|---|---|
$joinTo | string | Tabellenname oder Modellklassenname |
$selectFields | array|string | Spalten, die aus der gejointen Tabelle ausgewählt werden sollen |
$field1 | string|null | Join-Feld aus der gejointen Tabelle |
$field2 | string|null | Join-Feld aus der Haupttabelle |
$unselectOldFields | bool | Wenn true, wird das -Select der Haupttabelle entfernt |
$alias | string|null | Alias für die gejointen Tabelle |
Wenn eine Beziehung im Modell definiert ist, kannst du per Beziehungsname joinen, ohne die Felder manuell anzugeben:
// Appointment hat die Relation 'customer' definiert
$appointments = Appointment::query()
->leftJoin('customer', ['first_name', 'last_name'])
->fetchAll();
// Verwendet automatisch das Feldmapping der BeziehungAppointment::query()
->leftJoinSelf('parent', ['status'], 'id', 'recurring_id')
->fetchAll();
// Joint die appointments-Tabelle mit sich selbst unter dem Alias 'parent'Appointment::query()
->leftJoin('staffs', ['name'], 'staff_id', 'id', false, 'assigned_staff')
->fetchAll();
// Zugriff: $appointment->assigned_staff_nameCustomer::query()->insert([
'first_name' => 'John',
'last_name' => 'Doe',
'email' => '[email protected]',
'phone_number' => '+1234567890'
]);
// Die eingefügte ID abrufen
$newId = DB::lastInsertedId();
// oder
$newId = Customer::lastId();// Nach ID aktualisieren
Customer::query()
->whereId(5)
->update(['first_name' => 'Jane']);
// Mit Bedingungen aktualisieren
Appointment::query()
->where('status', 'pending')
->where('starts_at', '<', '2025-01-01 00:00:00')
->update(['status' => 'canceled']);Verwende DB::field(), wenn ein Update-Wert auf eine andere Spalte verweisen soll, statt als einfacher String behandelt zu werden:
Appointment::query()
->whereId($id)
->update([
'paid_amount' => DB::field('paid_amount + 100')
]);
// Erzeugt: SET paid_amount = paid_amount + 100// Nach ID löschen
Customer::query()->whereId(5)->delete();
// Mit Bedingungen löschen
Appointment::query()
->where('status', 'canceled')
->where('starts_at', '<', '2024-01-01 00:00:00')
->delete();Du kannst das generierte SQL prüfen, ohne die Abfrage auszuführen:
$sql = Appointment::query()
->where('status', 'approved')
->orderBy(['starts_at' => 'DESC'])
->limit(10)
->toSql();
error_log($sql);
// Output: SELECT * FROM wp_bkntc_appointments WHERE status = 'approved' ORDER BY starts_at DESC LIMIT 0, 10Wenn fetch() einen einzelnen Datensatz zurückgibt, wird dieser als Collection-Objekt zurückgegeben. Wenn fetchAll() verwendet wird, liefert es ein Array von Collection-Objekten.
$customer = Customer::get(1);
// Eigenschaftszugriff
echo $customer->first_name;
echo $customer->email;
// Array-Zugriff
echo $customer['first_name'];
echo $customer['email'];
// In einfaches Array umwandeln
$array = $customer->toArray();Modelle können virtuelle oder berechnete Attribute mit get{AttributeName}Attribute-Methoden definieren.
Beispiel in einem Customer-Modell:
public function getFullNameAttribute($customer)
{
return $customer->first_name . ' ' . $customer->last_name;
}
// Verwendung:
$customer = Customer::get(1);
echo $customer->full_name; // "John Doe"Beispiel in einem Appointment-Modell:
public static function getStatusNameAttribute($appointmentInf)
{
$statuses = Helper::getAppointmentStatuses();
return $statuses[$appointmentInf->status]['title'] ?? $appointmentInf->status;
}
// Verwendung:
$appointment = Appointment::get(1);
echo $appointment->status_name; // "Approved"Wenn fetch() keinen passenden Datensatz findet, wird null zurückgegeben, nicht eine leere Collection. Berücksichtige diesen Fall immer:
$customer = Customer::where('email', '[email protected]')->fetch();
if ($customer) {
echo $customer->first_name;
}Beziehungen erlauben es dir, zwischen verbundenen Modellen zu navigieren, und unterstützen außerdem das automatische Feldmapping in Joins.
Beziehungen werden im $relations-Array des Modells definiert:
public static $relations = [
'relationName' => [RelatedModel::class, 'foreignKey', 'localKey']
];| Parameter | Beschreibung |
|---|---|
RelatedModel::class | Die zugehörige Modellklasse |
foreignKey | Die Spalte in der zugehörigen Tabelle |
localKey | Die Spalte in dieser Tabelle |
Beispiel aus einem Appointment-Modell:
public static $relations = [
'extras' => [AppointmentExtra::class, 'appointment_id', 'id'],
'location' => [Location::class, 'id', 'location_id'],
'service' => [Service::class, 'id', 'service_id'],
'staff' => [Staff::class, 'id', 'staff_id'],
'customer' => [Customer::class, 'id', 'customer_id'],
'prices' => [AppointmentPrice::class, 'appointment_id', 'id'],
];Wenn du eine Beziehung als Methode auf einer Collection aufrufst, erhältst du einen QueryBuilder für die zugehörigen Datensätze:
$appointment = Appointment::get(1);
// Gibt einen QueryBuilder zurück — rufe fetch()/fetchAll() auf, um Ergebnisse zu erhalten
$customer = $appointment->customer()->fetch();
$extras = $appointment->extras()->fetchAll();
$service = $appointment->service()->fetch();
echo $customer->first_name;
echo $service->name;Für bessere Performance und zur Vermeidung von N+1-Abfragen verwende Joins:
$appointments = Appointment::query()
->leftJoin('customer', ['first_name', 'last_name', 'email'])
->leftJoin('service', ['name', 'price', 'duration'])
->leftJoin('staff', ['name'])
->where('status', 'approved')
->orderBy(['starts_at' => 'DESC'])
->limit(20)
->fetchAll();
foreach ($appointments as $apt) {
// Gejointen Spalten verwenden {relation}_{column} als Benennung
echo $apt->customer_first_name;
echo $apt->service_name;
echo $apt->staff_name;
echo $apt->starts_at;
}Beziehungen ermöglichen auch automatisches Feldmapping in Joins, sodass Foreign- und Local-Keys nicht manuell wiederholt werden müssen:
// Diese beiden Varianten sind gleichwertig:
Appointment::query()->leftJoin('customer', ['first_name'], 'customer_id', 'id');
Appointment::query()->leftJoin('customer', ['first_name']); // Verwendet die BeziehungsdefinitionGlobale Scopes wenden automatisch Bedingungen auf jede Abfrage eines Modells an. So handhabt Booknetic Funktionen wie Tenant-Isolierung und mitarbeiterbasierte Zugriffsbeschränkung.
class Appointment extends Model
{
use MultiTenant {
booted as private tenantBoot;
}
public static function booted()
{
self::tenantBoot(); // Fügt den tenant_id-Scope hinzu
// Mitarbeiter können nur ihre eigenen Termine sehen
self::addGlobalScope('staff_id', function (QueryBuilder $builder, $queryType) {
if (!Permission::isBackEnd() || Permission::isAdministrator()) {
return; // Keine Einschränkung für Frontend oder Admin-Benutzer
}
$builder->where('staff_id', Permission::myStaffId());
});
}
}Jede Abfrage auf Appointment enthält jetzt automatisch:
WHERE tenant_id = {current_tenant} aus MultiTenantWHERE staff_id = {current_staff} für Nicht-Admin-Backend-Benutzer$queryTypeDer Scope-Callback erhält den Typ der Abfrage, sodass du das Verhalten je nach Operation variieren kannst:
| queryType | Wann |
|---|---|
'select' | SELECT-Abfragen wie fetch, fetchAll, count und ähnliche Operationen |
'insert' | INSERT-Abfragen |
'update' | UPDATE-Abfragen |
'delete' | DELETE-Abfragen |
Du kannst einen bestimmten Scope bei Bedarf deaktivieren:
// Einen bestimmten Scope überspringen
Appointment::query()
->withoutGlobalScope('staff_id')
->fetchAll();
// Tenant-Scope überspringen
Appointment::query()
->noTenant()
->fetchAll();Das MultiTenant-Trait bietet automatische Tenant-Isolierung im SaaS-Modus.
Wenn ein Modell MultiTenant verwendet:
WHERE tenant_id = {current_tenant}tenant_id = {current_tenant}WHERE tenant_id IS NULLclass Customer extends Model
{
use MultiTenant;
}
// Im SaaS-Modus wird diese Abfrage automatisch zu:
// SELECT * FROM wp_bkntc_customers WHERE tenant_id = 5
Customer::query()->fetchAll();Verwende noTenant(), wenn du absichtlich über alle Tenants hinweg abfragen musst, zum Beispiel in Super-Admin-Logik:
// ALLE Kunden über alle Tenants hinweg abfragen
$allCustomers = Customer::query()
->noTenant()
->fetchAll();Einige Tabellen haben keine tenant_id-Spalte, zum Beispiel appointment_extras, appointment_prices und data. Ihre Modelle verwenden das MultiTenant-Trait einfach nicht, daher wird kein Tenant-Filter angewendet.
Modelle unterstützen Lifecycle-Hooks, die rund um CRUD-Operationen ausgelöst werden:
| Hook | Wird ausgelöst, wenn | Kann abbrechen? |
|---|---|---|
onCreating($closure) | Vor INSERT | Ja |
onCreated($closure) | Nach INSERT | Nein |
onUpdating($closure) | Vor UPDATE | Ja |
onUpdated($closure) | Nach UPDATE | Nein |
onDeleting($closure) | Vor DELETE | Ja |
onDeleted($closure) | Nach DELETE | Nein |
onRetrieving($closure) | Vor SELECT | Nein |
onRetrieved($closure) | Nach SELECT, pro Zeile | Nein |
Wenn ein before-Hook wie onCreating, onUpdating oder onDeleting false zurückgibt, wird die Operation abgebrochen.
// Das Löschen eines bestimmten Datensatzes verhindern
Customer::onDeleting(function ($customerId) {
if ($customerId === 1) {
return false; // Löschen von Kunde #1 verhindern
}
});
// Jeden neuen Termin protokollieren
Appointment::onCreated(function (QueryBuilder $queryBuilder) {
$data = $queryBuilder->getProperties();
error_log('Neuer Termin für Service: ' . $data['service_id']);
});Setze $timeStamps = true, um created_at und updated_at automatisch zu füllen:
class MyModel extends Model
{
protected static bool $timeStamps = true;
}Beim Einfügen werden sowohl created_at als auch updated_at automatisch gesetzt. Beim Aktualisieren wird updated_at automatisch aktualisiert.
Setze $enableOwnershipFields = true, um created_by und updated_by automatisch mit der aktuellen WordPress-Benutzer-ID zu füllen:
class MyModel extends Model
{
protected static bool $enableOwnershipFields = true;
}Jedes Modell unterstützt das Speichern beliebiger Key-Value-Daten über die Tabelle data. Das ist nützlich, wenn addon-spezifische Daten gespeichert werden müssen, ohne das Schema der Haupttabelle zu ändern.
// Meta-Daten für einen Kunden setzen
Customer::setData($customerId, 'preferred_language', 'en');
// Meta-Daten abrufen
$lang = Customer::getData($customerId, 'preferred_language', 'default_value');
// Meta-Daten löschen
Customer::deleteData($customerId, 'preferred_language');$customer = Customer::get(5);
// Setzen
$customer->setData('vip_level', 'gold');
// Abrufen
$level = $customer->getData('vip_level', 'standard');
// Löschen
$customer->deleteData('vip_level');Intern werden diese Daten in der Tabelle wp_bkntc_data gespeichert:
| Spalte | Beschreibung |
|---|---|
table_name | Ursprungstabelle, zum Beispiel customers |
row_id | Datensatz-ID |
data_key | Schlüssel |
data_value | Wert |
Wenn die ORM-Schicht nicht ausreicht oder du direkten Zugriff auf die Datenbankwerkzeuge von WordPress brauchst, kannst du direkt mit der Klasse DB arbeiten.
use BookneticApp\Providers\DB\DB;
// Den vollständigen Tabellennamen mit Präfix abrufen
DB::table('appointments'); // "wp_bkntc_appointments"
DB::table(Appointment::class); // "wp_bkntc_appointments"// Vorbereitete Abfrage (sicher gegen SQL-Injection)
$sql = DB::raw("SELECT * FROM %s WHERE status = %s", [
DB::table('appointments'),
'approved'
]);
// Über WordPress $wpdb ausführen
$results = DB::DB()->get_results($sql, ARRAY_A);// Die WordPress-$wpdb-Instanz abrufen
$wpdb = DB::DB();
// Beliebige $wpdb-Methode verwenden
$wpdb->query("...");
$wpdb->get_row("...");
$wpdb->get_results("...");// Caching aktivieren — identische Abfragen geben gecachte Ergebnisse zurück
DB::enableCache();
// Deine Abfragen hier ...
$a = Customer::get(1); // Trifft die Datenbank
$b = Customer::get(1); // Gibt gecachtes Ergebnis zurück
// Cache deaktivieren und leeren
DB::disableCache();Customer::query()->insert([...]);
$id = DB::lastInsertedId();
// oder
$id = Customer::lastId();Verwende DB::field(), wenn eine Abfrage auf einen Spaltennamen verweisen soll, statt auf einen literalen Stringwert:
// Zwei Spalten vergleichen
Appointment::where('starts_at', '>', DB::field('busy_from'))->fetchAll();
// Mit dem Wert einer anderen Spalte aktualisieren
Appointment::query()->whereId($id)->update([
'paid_amount' => DB::field('paid_amount + 50')
]);Modelle, die mehrsprachige Inhalte unterstützen, verwenden das Translator-Trait:
class Service extends Model
{
use Translator, MultiTenant;
protected static $translations = ['name', 'note'];
}Um übersetzte Werte abzurufen, rufe withTranslations() auf:
// Mit angewendeten Übersetzungen abrufen (basierend auf der aktuellen Locale)
$services = Service::query()
->withTranslations()
->fetchAll();
echo $services[0]->name; // Gibt den übersetzten Namen zurück, wenn vorhandenDies sind einige häufige ORM-Verwendungsmuster in der Booknetic-Entwicklung.
$page = 1;
$perPage = 15;
$query = Appointment::query()
->where('status', 'approved')
->orderBy(['starts_at' => 'DESC']);
$total = $query->count();
$appointments = $query
->limit($perPage)
->offset(($page - 1) * $perPage)
->fetchAll();$report = Appointment::query()
->select([
'service_id',
'COUNT(*) as total_bookings',
'SUM(paid_amount) as total_revenue'
], true)
->leftJoin('service', ['name'])
->where('status', 'approved')
->where('starts_at', '>=', '2025-01-01 00:00:00')
->groupBy(['service_id'])
->orderBy(['total_bookings' => 'DESC'])
->fetchAll();
foreach ($report as $row) {
echo $row->service_name . ': ' . $row->total_bookings . ' Buchungen, $' . $row->total_revenue;
}$query = Appointment::query()
->orderBy(['starts_at' => 'DESC']);
if ($statusFilter) {
$query->where('status', $statusFilter);
}
if ($staffId) {
$query->where('staff_id', $staffId);
}
if ($search) {
$query->leftJoin('customer', ['first_name', 'last_name']);
$query->like('customers.first_name', $search);
}
$results = $query->limit(20)->fetchAll();$exists = Customer::query()
->where('email', '[email protected]')
->count() > 0;$customer = Customer::where('email', $email)->fetch();
if (!$customer) {
Customer::query()->insert([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email
]);
$customer = Customer::get(Customer::lastId());
}Das ORM und der Query Builder von Booknetic bieten eine vollständige modellbasierte Datenbankschicht mit ausdrucksstarkem Abfrageaufbau, Unterstützung für Beziehungen, automatischer Tenant-Isolierung, Lifecycle-Hooks, Übersetzungsunterstützung, Key-Value-Metadaten und direktem Low-Level-Datenbankzugriff, wenn nötig.
| Operation | Code |
|---|---|
| Nach ID abrufen | Model::get($id) |
| Einen Datensatz finden | Model::where('field', 'value')->fetch() |
| Mehrere Datensätze finden | Model::where('field', 'value')->fetchAll() |
| Count | Model::where('field', 'value')->count() |
| Sum | Model::where('field', 'value')->sum('column') |
| Einfügen | Model::query()->insert([...]) |
| Aktualisieren | Model::whereId($id)->update([...]) |
| Löschen | Model::whereId($id)->delete() |
| Join | Model::query()->leftJoin('table', ['cols'], 'fk', 'pk')->fetchAll() |
| SubQuery | Model::query()->selectSubQuery($subQB, 'alias')->fetchAll() |
| Ohne Tenant | Model::query()->noTenant()->fetchAll() |
| Scope überspringen | Model::query()->withoutGlobalScope('name')->fetchAll() |
| Übersetzt | Model::query()->withTranslations()->fetchAll() |
| SQL debuggen | Model::query()->where(...)->toSql() |
| Meta abrufen | Model::getData($id, 'key', 'default') |
| Meta setzen | Model::setData($id, 'key', 'value') |