CRUD Operations
Revision: 2025/10/13
The Model provides comprehensive methods for Create, Read, Update, and Delete operations. There are two main approaches: the quick store() method and the classic fill() + validate() + save() workflow.
- Quick Method:
store($data, $id)- Direct save without validation - Classic Method:
fill() + validate() + save()- With full validation support
READ Operations
getById($id, bool $use_cache = true): static
Retrieves a single record by primary key. Always returns a Model instance. Use isEmpty() to check if the record was found.
/**
* @param mixed $id Primary key value
* @param bool $use_cache Whether to use internal cache
* @return static Always returns Model instance (use isEmpty() to check if record exists)
*/
public function getById($id, bool $use_cache = true): static;
// Example: Get product by ID
$product = $model->getById(123);
if (!$product->isEmpty()) {
echo "Product: " . $product->name;
echo "Price: €" . $product->price;
echo "Stock: " . ($product->in_stock ? 'Yes' : 'No');
} else {
echo "Product not found";
}
// Example: Disable cache
$product = $model->getById(123, false);
isEmpty() or count() to check if the record exists:
if (!$product->isEmpty()) or if ($product->count() > 0)
getByIds(string|array $ids): static
Retrieves multiple records by their IDs. Always returns a Model instance. Use isEmpty() to check if records were found.
/**
* @param string|array $ids Array of IDs or comma-separated string
* @return static Always returns Model instance (use isEmpty() to check if records exist)
*/
public function getByIds(string|array $ids): static;
// Example: Array of IDs
$products = $model->getByIds([1, 5, 10, 15]);
if (!$products->isEmpty()) {
echo "Found: " . $products->count() . " products\n";
foreach ($products as $product) {
echo "- " . $product->name . "\n";
}
}
// Example: Comma-separated string
$products = $model->getByIds('1,5,10,15');
getByIdAndUpdate($id, array $merge_data = []): static
Retrieves a record by ID or returns an empty object if not found. Useful for edit forms.
/**
* @param mixed $id Primary key value
* @param array $merge_data Data to merge with the record
* @return static Model instance (existing record or empty)
*/
public function getByIdAndUpdate($id, array $merge_data = []): static;
// Example: Edit form
$id = $_GET['id'] ?? 0;
$product = $model->getByIdAndUpdate($id, $_POST);
// If ID exists and found: returns that record with $_POST merged
// If ID is 0 or not found: returns empty object with $_POST data
// Now you can use it directly in forms
echo $product->name; // Works for both new and existing records
echo $product->price; // Works for both new and existing records
getEmpty(array $data = []): static
Returns an empty Model instance for creating new records.
/**
* @param array $data Optional data to initialize the object
* @return static Empty Model instance
*/
public function getEmpty(array $data = []): static;
// Example: Create new record
$product = $model->getEmpty([
'name' => 'New Product',
'price' => 29.99,
'in_stock' => true
]);
// Example: From POST data
$product = $model->getEmpty($_POST);
CREATE & UPDATE Operations
Method 1: Quick Save with store()
The fastest way to save data - directly inserts or updates without validation.
/**
* @param array $data Data to save
* @param mixed $id Primary key for UPDATE (null for INSERT)
* @return bool|int Insert ID on success, false on failure
*/
public function store(array $data, $id = null): bool|int;
// Example: INSERT new record
$id = $model->store([
'name' => 'Laptop Pro',
'price' => 1299.99,
'description' => 'High-performance laptop',
'in_stock' => true,
'created_at' => date('Y-m-d H:i:s')
]);
if ($id) {
echo "Product created with ID: $id";
} else {
echo "Error: " . $model->getLastError();
}
// Example: UPDATE existing record
$result = $model->store([
'name' => 'Laptop Pro - Updated',
'price' => 1199.99
], 123); // Pass ID as second parameter
if ($result) {
echo "Product updated successfully";
}
// Example: From POST data
$id = $_POST['id'] ?? null;
$result = $model->store($_POST, $id);
if ($result) {
if ($id) {
echo "Updated successfully";
} else {
echo "Created with ID: $result";
}
}
Method 2: Classic Workflow with fill() + validate() + save()
The recommended approach for forms with validation requirements.
Step 1: Get or Create Object
// For new record
$product = $model->getEmpty();
// For existing record
$product = $model->getById($id);
// For edit form (new or existing)
$product = $model->getByIdAndUpdate($id);
Step 2: Fill with Data
/**
* Fills the model with data
* @param array $data Data to fill
* @return void
*/
public function fill(array $data): void;
// Example: Fill from POST
$product->fill($_POST);
// Example: Fill from array
$product->fill([
'name' => 'Product Name',
'price' => 29.99,
'in_stock' => true
]);
// Example: Fill specific fields
$product->fill([
'price' => 24.99,
'description' => 'Updated description'
]);
Step 3: Validate
/**
* Validates data according to rules defined in configure()
* @param bool $validate_all Validate all records in result set
* @return bool True if valid, false if validation fails
*/
public function validate(bool $validate_all = false): bool;
// Example: Validate before save
if ($product->validate()) {
echo "Data is valid";
} else {
echo "Validation failed";
// Errors are stored in MessagesHandler
$errors = MessagesHandler::getErrors();
}
Step 4: Save
/**
* Saves the model data to database
* @return bool True on success, false on failure
*/
public function save(): bool;
// Example: Complete workflow
$product = $model->getEmpty($_POST);
if ($product->validate()) {
if ($product->save()) {
$id = $product->getLastInsertId();
echo "Saved successfully with ID: $id";
} else {
echo "Save error: " . $product->getLastError();
}
} else {
echo "Validation failed";
foreach (MessagesHandler::getErrors() as $error) {
echo "- $error\n";
}
}
Complete Example: Edit Form Handler
protected function actionSaveProduct() {
$id = $_POST['id'] ?? 0;
// Get existing or create new
$product = $this->model->getByIdAndUpdate($id, $_POST);
// Validate
if (!$product->validate()) {
// Redirect back with errors
Route::redirectError(
$_POST['url_error'],
'Validation failed',
$_POST
);
return;
}
// Save
if ($product->save()) {
// Get ID for new records
if ($id == 0) {
$id = $product->getLastInsertId();
}
Route::redirectSuccess(
'?page=products&action=edit&id=' . $id,
'Product saved successfully'
);
} else {
Route::redirectError(
$_POST['url_error'],
'Error: ' . $this->model->getLastError(),
$_POST
);
}
}
Batch Operations
Multiple Records with fill()
You can fill multiple records and save them in batch:
$model = new ProductsModel();
// Fill multiple records
$model->fill(['name' => 'Product 1', 'price' => 10]);
$model->fill(['name' => 'Product 2', 'price' => 20]);
$model->fill(['name' => 'Product 3', 'price' => 30]);
// Save all at once
if ($model->save()) {
$results = $model->getCommitResults();
echo "Saved " . count($results) . " records:\n";
foreach ($results as $result) {
if ($result['result']) {
echo "- ID: {$result['id']}, Action: {$result['action']}\n";
} else {
echo "- Error: {$result['last_error']}\n";
}
}
}
DELETE Operations
delete($id = null): bool
Deletes a record by primary key. If $id is not provided, it only works when exactly one record is loaded in records_objects; otherwise it throws an exception to prevent accidental deletions.
/**
* @param mixed|null $id Primary key value. If null, exactly one record must be loaded in records_objects
* @return bool True on success, false on failure
*/
public function delete($id = null): bool;
// Example: Delete single record
if ($model->delete(123)) {
echo "Product deleted";
} else {
echo "Delete failed: " . $model->getLastError();
}
// Example: Delete first stored record (only if exactly one record is loaded)
if ($model->delete()) {
echo "First stored record deleted";
}
// Example: Delete with confirmation
$id = $_GET['id'] ?? 0;
$product = $model->getById($id);
if (!$product || $product->count() == 0) {
echo "Product not found";
exit;
}
if ($model->delete($id)) {
// Verify deletion
$verify = $model->getById($id);
if (!$verify || $verify->count() == 0) {
echo "Product deleted successfully";
}
}
// Example: In module action
protected function tableActionDeleteProduct($id, $request) {
if ($this->model->delete($id)) {
return true;
} else {
MessagesHandler::addError($this->model->getLastError());
return false;
}
}
deleteAll(): bool
Deletes all stored records from the database.
/**
* @return bool True on success, false on failure
*/
public function deleteAll(): bool;
// Example: Delete all stored records
if ($model->deleteAll()) {
echo "All records deleted";
} else {
echo "Delete failed: " . $model->getLastError();
}
detach(): void
Marks the current record for deletion. The record will be deleted when save() is called.
// Load and mark for deletion
$product = $model->getById(5);
$product->detach(); // Mark for deletion
$product->save(); // Actually deletes the record
// Useful in forms where you want to delete after validation
$product = $model->getById($_POST['id']);
if ($_POST['action'] === 'delete') {
$product->detach();
}
$product->save(); // Will delete if detached
Lifecycle Hooks
Override these methods in your Model to intercept and customize save/delete operations:
beforeSave(&$records): bool
Called before saving records. Return false to prevent save.
public function beforeSave(&$records): bool
{
foreach ($records as &$record) {
// Check if insert or update
if ($record['___action'] === 'insert') {
$record['CREATED_AT'] = date('Y-m-d H:i:s');
$record['CREATED_BY'] = Get::user()->id;
} elseif ($record['___action'] === 'update') {
$record['UPDATED_AT'] = date('Y-m-d H:i:s');
$record['UPDATED_BY'] = Get::user()->id;
}
// Auto-generate slug if empty
if (empty($record['SLUG']) && !empty($record['TITLE'])) {
$record['SLUG'] = $this->generateSlug($record['TITLE']);
}
// Prevent save based on business logic
if ($record['PRICE'] < 0) {
$this->last_error = "Price cannot be negative";
return false; // Prevents save
}
}
return true; // Allow save
}
___action key in the record array indicates the operation type:
'insert'- New record being created'update'- Existing record being modified'delete'- Record being deleted (in beforeDelete/afterDelete)
afterSave($saved_data, $results): void
Called after successful save. Use for notifications, cache clearing, logging, etc.
public function afterSave($saved_data, $results): void
{
foreach ($saved_data as $record) {
if ($record['___action'] === 'insert') {
// Send notification for new records
$this->sendNotification("New product created: " . $record['NAME']);
// Clear cache
Cache::forget('products_list');
// Log activity
ActivityLog::create([
'action' => 'product_created',
'product_id' => $record['ID'],
'user_id' => Get::user()->id
]);
}
}
}
beforeDelete($ids): bool
Called before deleting records. Return false to prevent deletion.
public function beforeDelete($ids): bool
{
// Check if products have orders
foreach ($ids as $id) {
$orders = (new OrdersModel())
->where('PRODUCT_ID = ?', [$id])
->count();
if ($orders > 0) {
$this->last_error = "Cannot delete product with existing orders";
return false; // Prevents deletion
}
}
return true; // Allow deletion
}
afterDelete($ids): void
Called after successful deletion. Use for cleanup, cache clearing, logging, etc.
public function afterDelete($ids): void
{
foreach ($ids as $id) {
// Delete related files
$this->deleteProductImages($id);
// Clear cache
Cache::forget("product_{$id}");
// Log activity
ActivityLog::create([
'action' => 'product_deleted',
'product_id' => $id,
'user_id' => Get::user()->id
]);
}
}
Data Formatting
The Model automatically handles data type conversions. You can control the format of data:
$product = $model->getById(1);
// Set formatted mode (for display)
$product->setOutputMode('formatted');
echo $product->created_at; // DateTime object
// Set SQL mode (for database)
$product->setOutputMode('sql');
echo $product->created_at; // '2025-01-15 10:30:00' string
// Set raw mode
$product->setOutputMode('raw');
echo $product->created_at; // Original value
// Get as array
$data = $product->toArray(); // Formatted
$data = $product->toArray('sql'); // SQL format
$data = $product->toArray('raw'); // Raw format
Utility Methods
getLastInsertId(): int
// After INSERT
$id = $model->store($data);
// or
$model->save();
$id = $model->getLastInsertId();
getLastError(): string
if (!$model->store($data)) {
echo "Error: " . $model->getLastError();
}
hasError(): bool
$model->save();
if ($model->hasError()) {
echo "An error occurred";
}
getCommitResults(): array
// After save(), get detailed results
$results = $model->getCommitResults();
foreach ($results as $result) {
echo "ID: {$result['id']}\n";
echo "Action: {$result['action']}\n"; // 'insert' or 'edit'
echo "Result: " . ($result['result'] ? 'success' : 'failed') . "\n";
if (!$result['result']) {
echo "Error: {$result['last_error']}\n";
}
}
Best Practices
- Use
store()for simple, already-validated data - Use
fill() + validate() + save()for form submissions - Always check
count() > 0aftergetById() - Use
getByIdAndUpdate()for edit forms to handle both new and existing records - Check
getLastError()when operations fail - Use transactions for multiple related operations (see Database docs)
See Also
- Model Overview - General concepts
- Query Builder - Building complex queries
- Schema - Table management