Model Extensions
Model Extensions allow you to add fields, validation, data processing, and lifecycle hooks to your models without modifying the model class itself. They extend the AbstractModelExtension class.
Creating a Model Extension
namespace Extensions\MyExtension;
use App\Abstracts\{AbstractModelExtension, RuleBuilder};
use App\Attributes\{ToDatabaseValue, ToDisplayValue};
class Model extends AbstractModelExtension
{
// Configuration parameters
protected $my_option = true;
// Add fields during configuration
public function configure(RuleBuilder $rule_builder): void
{
$rule_builder
->int('my_field')
->default(0);
}
// Process data before saving
#[ToDatabaseValue('my_field')]
public function processMyField($current_record)
{
return $current_record->my_field * 2;
}
// Format data for display
#[ToDisplayValue('my_field')]
public function formatMyField($current_record)
{
return "Value: " . $current_record->my_field;
}
}
Extension Parameters
Extensions can have configurable parameters defined as protected properties:
class Model extends AbstractModelExtension
{
protected $show_username = true;
protected $show_email = false;
protected $max_records = 100;
}
Pass parameters when adding the extension:
$rule_builder->addExtension('MyExtension', [
'show_username' => false,
'max_records' => 50
]);
Configuration Hook
The configure() method is called during model initialization. Use it to add fields:
public function configure(RuleBuilder $rule_builder): void
{
$rule_builder
->int('created_by')
->nullable(true)
->default(0)
->label('Created By')
->timestamp('created_at')
->default('CURRENT_TIMESTAMP')
->label('Created At');
}
Attribute-Based Methods
Model extensions support the same attributes as models for automatic method registration:
#[ToDatabaseValue('field_name')]
Process field value before saving to database:
#[ToDatabaseValue('created_by')]
public function setCreatedBy($current_record)
{
// Auto-fill with current user ID on insert
if (empty($current_record->created_by)) {
$user = Get::make('Auth')->getUser();
return $user->id ?? 0;
}
return $current_record->created_by;
}
#[ToDisplayValue('field_name')]
Format field value for display:
#[ToDisplayValue('created_by')]
public function formatCreatedBy($current_record)
{
$user = Get::make('Auth')->getUser($current_record->created_by);
return $user ? $user->username : '-';
}
#[SetValue('field_name')]
Customize how field values are set:
#[SetValue('tags')]
public function setTags($current_record)
{
// Convert array to comma-separated string
if (is_array($current_record->tags)) {
return implode(',', $current_record->tags);
}
return $current_record->tags;
}
#[Validate('field_name')]
Add custom validation:
#[Validate('title')]
public function validateTitle($value, $field_name, $current_record)
{
if (strlen($value) < 3) {
return 'Title must be at least 3 characters';
}
return true;
}
Lifecycle Hooks
| Hook | Parameters | Description |
|---|---|---|
configure() |
RuleBuilder $rule_builder |
Called during model configuration to add fields and rules |
afterSave() |
array $records, array $results |
Called after records are saved (insert/update) |
beforeDelete() |
array $ids |
Called before records are deleted |
afterDelete() |
array $ids |
Called after records are deleted |
afterSave() Example
public function afterSave($records_array, $save_results)
{
foreach ($save_results as $result) {
$action = $result['action']; // 'insert' or 'edit'
$id = $result['id'];
// Log the save operation
error_log("Record {$id} was {$action}ed");
}
}
afterDelete() Example
public function afterDelete($ids)
{
foreach ($ids as $id) {
// Clean up related data
error_log("Record {$id} was deleted");
}
}
Accessing the Parent Model
Extensions have access to the parent model via $this->model->get():
public function myMethod()
{
$model = $this->model->get();
$table = $model->getTable();
$primary_key = $model->getPrimaryKey();
$rules = $model->getRules();
// Use model methods
$record = $model->getById(123);
}
Complete Example: Audit Trail Extension
This extension creates a complete audit trail by saving record snapshots:
namespace Extensions\Audit;
class Model extends AbstractModelExtension
{
protected static $maxAuditRecords = 0; // 0 = unlimited
public function configure(RuleBuilder $rule_builder): void
{
// Audit data is stored in a separate table
// No fields needed in main model
}
public function afterSave($records_array, $save_results)
{
$parent_model = $this->model->get();
$table_audit = $parent_model->getTable() . "_audit";
$auditModel = new AuditModel();
foreach ($save_results as $result) {
$action = $result['action'];
$recordId = $result['id'];
// Find the saved record data
$recordData = null;
foreach ($records_array as $record) {
if ($record[$parent_model->getPrimaryKey()] == $recordId) {
$recordData = $record;
break;
}
}
// Save to audit table
$audit_record = $recordData;
$audit_record['audit_action'] = $action;
$audit_record['audit_record_id'] = $recordId;
$audit_record['audit_timestamp'] = time();
$audit_record['audit_user_id'] = Get::make('Auth')->getUser()->id ?? 0;
$auditModel->store($audit_record);
}
}
public function afterDelete($ids)
{
$auditModel = new AuditModel();
foreach ($ids as $deleted_id) {
$audit_record = [
'audit_action' => 'delete',
'audit_record_id' => $deleted_id,
'audit_timestamp' => time(),
'audit_user_id' => Get::make('Auth')->getUser()->id ?? 0
];
$auditModel->store($audit_record);
}
}
}
Best Practices
- Keep extensions focused - Each extension should handle one specific behavior
- Use parameters - Make extensions configurable for different use cases
- Document parameters - Add docblocks to protected properties
- Handle edge cases - Check for null values and missing data
- Use WeakReference - The parent model is stored as a WeakReference to prevent memory leaks
- Cache data - Use static caches for frequently accessed data (like user lookups)
Testing Extensions
Test your extension by adding it to a model:
class TestModel extends AbstractModel
{
public function configure(RuleBuilder $rule_builder): void
{
$rule_builder
->table('#__test')
->id()
->string('title', 255)
// Add your extension
->addExtension('MyExtension', [
'my_option' => true
]);
}
}