v5 ist live

BIS ZU

60% RABATT
Heißer Dezember-Sale
Launch-Sale
0 TAGE
:
0 STUNDEN
:
0 MINUTEN
:
0 SEKUNDEN
JETZT ERHALTEN percent icon

ORM und Query‑Builder

Das ORM und den Query-Builder für Modelle, Relationen, Multi‑Tenancy, Hooks und direkten Datenbankzugriff erlernen.

Version:
Kategorien

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.

Architekturüberblick

Das ORM basiert auf vier Kernklassen:

KlasseNamespaceRolle
ModelBookneticApp\Providers\DB\ModelBasisklasse für alle Modelle. Definiert Tabelle, Beziehungen, Scopes und Lifecycle-Hooks.
QueryBuilderBookneticApp\Providers\DB\QueryBuilderVerkettbarer Query Builder. Behandelt WHERE, JOIN, ORDER BY und ähnliche Abfrageoperationen.
CollectionBookneticApp\Providers\DB\CollectionKapselt eine einzelne Datenbankzeile. Bietet Eigenschaftszugriff, Array-Zugriff und das Laden von Beziehungen.
DBBookneticApp\Providers\DB\DBLow-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

Modelle repräsentieren Datenbanktabellen und dienen als Einstiegspunkt für die meisten ORM-Operationen.

Ein Modell definieren

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.

Namenskonvention für Tabellen

Tabellennamen werden automatisch aus dem Klassennamen nach dieser Regel generiert:

PascalCase → snake_case + Plural

ModellklasseGenerierte TabelleVollständige DB-Tabelle
Customercustomerswp_bkntc_customers
Appointmentappointmentswp_bkntc_appointments
ServiceExtraservice_extraswp_bkntc_service_extras
AppointmentPriceappointment_priceswp_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';
}

Verfügbare Kernmodelle

Die folgenden integrierten Modelle werden häufig verwendet:

ModellTabelleMulti-TenantBeschreibung
AppointmentappointmentsJaBuchungen
AppointmentExtraappointment_extrasNeinExtras, die an Termine angehängt sind
AppointmentPriceappointment_pricesNeinPreisaufschlüsselung pro Termin
CustomercustomersJaKundendatensätze
CustomerCategorycustomer_categoriesJaKundengruppierung
ServiceservicesJaVerfügbare Services
ServiceCategoryservice_categoriesJaService-Gruppierung
ServiceExtraservice_extrasJaZusatz-Extras für Services
ServiceStaffservice_staffsNeinZuordnung Service–Mitarbeiter
StaffstaffsJaMitarbeiter
LocationlocationsJaGeschäftsstandorte
HolidayholidaysJaFreie Tage
SpecialDayspecial_daysJaBesondere Zeitplan-Überschreibungen
TimesheettimesheetsJaArbeitszeiten
WorkflowworkflowsJaAutomatisierungsregeln
WorkflowLogworkflow_logsJaAusführungsprotokolle von Automatisierungen
AppearanceappearancesJaTheme-Einstellungen des Buchungspanels
DatadataNeinKey-Value-Metaspeicher
TranslationtranslationsNeinMehrsprachige Inhalte

Query Builder

Der Query Builder ist die zentrale Schnittstelle zum Abrufen, Filtern, Verknüpfen, Aktualisieren und Löschen von Datensätzen.

Eine Abfrage starten

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);

Daten abrufen

Eine einzelne Zeile abrufen

// 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;

Mehrere Zeilen abrufen

// 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;
}

Als einfache Arrays abrufen

// Gibt ein Array von assoziativen Arrays zurück (ohne Collection-Wrapper)
$rows = Customer::query()
    ->where('gender', 'male')
    ->fetchAllAsArray();

Nach ID abrufen

$appointment = Appointment::get(42);
// Entspricht: Appointment::query()->where('id', 42)->fetch();

WHERE-Bedingungen

Einfaches Where

// 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();

Array-Where

// Mehrere Bedingungen als Array übergeben
Appointment::where([
    'status'   => 'approved',
    'staff_id' => 3
])->fetchAll();

OR Where

Customer::query()
    ->where('first_name', 'John')
    ->orWhere('first_name', 'Jane')
    ->fetchAll();

Where ID

// Kurzform für where('id', $value)
Customer::whereId(5)->fetch();

NULL-Prüfungen

// 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();

IN-Klausel

// Ein Array als Wert übergeben
Appointment::where('status', ['approved', 'pending'])->fetchAll();

// NOT IN
Appointment::where('status', 'not in', ['canceled', 'rejected'])->fetchAll();

LIKE

// 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();

FIND_IN_SET

Für Spalten mit kommagetrennten Werten:

Service::query()->whereFindInSet('staff_ids', 5)->fetchAll();
// Erzeugt: WHERE FIND_IN_SET('5', staff_ids)

Gruppierte Bedingungen (verschachteltes Where)

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'

SubQuery in Where

// 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 ...)

Spalten auswählen

// 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_count

Sortierung

Appointment::query()
    ->orderBy(['starts_at' => 'DESC'])
    ->fetchAll();

// Mehrere Sortierspalten
Appointment::query()
    ->orderBy(['status' => 'ASC', 'starts_at' => 'DESC'])
    ->fetchAll();

Gruppierung

Appointment::query()
    ->select(['status', 'COUNT(*) as count'])
    ->groupBy(['status'])
    ->fetchAll();

Pagination

$page = 2;
$perPage = 10;

$appointments = Appointment::query()
    ->orderBy(['starts_at' => 'DESC'])
    ->limit($perPage)
    ->offset(($page - 1) * $perPage)
    ->fetchAll();

Aggregate

// 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');

Joins

Einfacher Join

$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(...)
ParameterTypBeschreibung
$joinTostringTabellenname oder Modellklassenname
$selectFieldsarray|stringSpalten, die aus der gejointen Tabelle ausgewählt werden sollen
$field1string|nullJoin-Feld aus der gejointen Tabelle
$field2string|nullJoin-Feld aus der Haupttabelle
$unselectOldFieldsboolWenn true, wird das -Select der Haupttabelle entfernt
$aliasstring|nullAlias für die gejointen Tabelle

Beziehungsbasierter Join

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 Beziehung

Self Join

Appointment::query()
    ->leftJoinSelf('parent', ['status'], 'id', 'recurring_id')
    ->fetchAll();
// Joint die appointments-Tabelle mit sich selbst unter dem Alias 'parent'

Join mit Alias

Appointment::query()
    ->leftJoin('staffs', ['name'], 'staff_id', 'id', false, 'assigned_staff')
    ->fetchAll();

// Zugriff: $appointment->assigned_staff_name

Insert

Customer::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();

Update

// 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']);

Aktualisieren mit Raw-Feldreferenzen

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

Delete

// 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();

Debugging

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, 10

Collection

Wenn fetch() einen einzelnen Datensatz zurückgibt, wird dieser als Collection-Objekt zurückgegeben. Wenn fetchAll() verwendet wird, liefert es ein Array von Collection-Objekten.

Auf Daten zugreifen

$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();

Berechnete Attribute

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"

Null-Sicherheit

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

Beziehungen erlauben es dir, zwischen verbundenen Modellen zu navigieren, und unterstützen außerdem das automatische Feldmapping in Joins.

Beziehungen definieren

Beziehungen werden im $relations-Array des Modells definiert:

public static $relations = [
    'relationName' => [RelatedModel::class, 'foreignKey', 'localKey']
];
ParameterBeschreibung
RelatedModel::classDie zugehörige Modellklasse
foreignKeyDie Spalte in der zugehörigen Tabelle
localKeyDie 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'],
];

Beziehungen verwenden

Lazy Loading

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;

Eager Loading per Join

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 Beziehungsdefinition

Globale Scopes

Globale Scopes wenden automatisch Bedingungen auf jede Abfrage eines Modells an. So handhabt Booknetic Funktionen wie Tenant-Isolierung und mitarbeiterbasierte Zugriffsbeschränkung.

Wie Scopes funktionieren

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 MultiTenant
  • WHERE staff_id = {current_staff} für Nicht-Admin-Backend-Benutzer

Der Parameter $queryType

Der Scope-Callback erhält den Typ der Abfrage, sodass du das Verhalten je nach Operation variieren kannst:

queryTypeWann
'select'SELECT-Abfragen wie fetch, fetchAll, count und ähnliche Operationen
'insert'INSERT-Abfragen
'update'UPDATE-Abfragen
'delete'DELETE-Abfragen

Einen Scope deaktivieren

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();

Multi-Tenancy

Das MultiTenant-Trait bietet automatische Tenant-Isolierung im SaaS-Modus.

Wie es funktioniert

Wenn ein Modell MultiTenant verwendet:

  • SELECT / UPDATE / DELETE-Abfragen enthalten automatisch WHERE tenant_id = {current_tenant}
  • INSERT-Abfragen setzen automatisch tenant_id = {current_tenant}
  • Im Nicht-SaaS-Modus erfolgt das Filtern mit WHERE tenant_id IS NULL
class 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();

Tenant-Filter umgehen

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();

Modelle ohne Multi-Tenancy

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.

Lifecycle-Hooks

Modelle unterstützen Lifecycle-Hooks, die rund um CRUD-Operationen ausgelöst werden:

HookWird ausgelöst, wennKann abbrechen?
onCreating($closure)Vor INSERTJa
onCreated($closure)Nach INSERTNein
onUpdating($closure)Vor UPDATEJa
onUpdated($closure)Nach UPDATENein
onDeleting($closure)Vor DELETEJa
onDeleted($closure)Nach DELETENein
onRetrieving($closure)Vor SELECTNein
onRetrieved($closure)Nach SELECT, pro ZeileNein

Wenn ein before-Hook wie onCreating, onUpdating oder onDeleting false zurückgibt, wird die Operation abgebrochen.

Verwendung

// 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']);
});

Automatische Zeitstempel

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.

Automatische Ownership-Felder

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;
}

Meta-Daten (Key-Value-Speicher)

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.

Auf einem Modell (statisch)

// 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');

Auf einer Collection-Instanz

$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:

SpalteBeschreibung
table_nameUrsprungstabelle, zum Beispiel customers
row_idDatensatz-ID
data_keySchlüssel
data_valueWert

DB-Klasse (Low-Level)

Wenn die ORM-Schicht nicht ausreicht oder du direkten Zugriff auf die Datenbankwerkzeuge von WordPress brauchst, kannst du direkt mit der Klasse DB arbeiten.

Auflösung von Tabellennamen

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"

Raw Queries

// 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);

Direkter Datenbankzugriff

// Die WordPress-$wpdb-Instanz abrufen
$wpdb = DB::DB();

// Beliebige $wpdb-Methode verwenden
$wpdb->query("...");
$wpdb->get_row("...");
$wpdb->get_results("...");

Query-Caching

// 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();

Letzte eingefügte ID

Customer::query()->insert([...]);
$id = DB::lastInsertedId();
// oder
$id = Customer::lastId();

Feldreferenzen

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')
]);

Übersetzungen

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 vorhanden

Häufige Muster

Dies sind einige häufige ORM-Verwendungsmuster in der Booknetic-Entwicklung.

Pagination mit Gesamtanzahl

$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();

Komplexe Report-Abfrage

$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;
}

Bedingtes Aufbauen von Abfragen

$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();

Prüfen, ob ein Datensatz existiert

$exists = Customer::query()
    ->where('email', '[email protected]')
    ->count() > 0;

Get-or-Create-Muster

$customer = Customer::where('email', $email)->fetch();

if (!$customer) {
    Customer::query()->insert([
        'first_name' => $firstName,
        'last_name'  => $lastName,
        'email'      => $email
    ]);
    $customer = Customer::get(Customer::lastId());
}

Zusammenfassung

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.

OperationCode
Nach ID abrufenModel::get($id)
Einen Datensatz findenModel::where('field', 'value')->fetch()
Mehrere Datensätze findenModel::where('field', 'value')->fetchAll()
CountModel::where('field', 'value')->count()
SumModel::where('field', 'value')->sum('column')
EinfügenModel::query()->insert([...])
AktualisierenModel::whereId($id)->update([...])
LöschenModel::whereId($id)->delete()
JoinModel::query()->leftJoin('table', ['cols'], 'fk', 'pk')->fetchAll()
SubQueryModel::query()->selectSubQuery($subQB, 'alias')->fetchAll()
Ohne TenantModel::query()->noTenant()->fetchAll()
Scope überspringenModel::query()->withoutGlobalScope('name')->fetchAll()
ÜbersetztModel::query()->withTranslations()->fetchAll()
SQL debuggenModel::query()->where(...)->toSql()
Meta abrufenModel::getData($id, 'key', 'default')
Meta setzenModel::setData($id, 'key', 'value')