FormBuilder Extensions
FormBuilder Extensions allow you to modify form behavior, add custom fields, buttons, and logic to forms without modifying the FormBuilder class itself. They extend the AbstractFormBuilderExtension class.
Creating a FormBuilder Extension
namespace Extensions\MyExtension;
use App\Abstracts\AbstractFormBuilderExtension;
class FormBuilder extends AbstractFormBuilderExtension
{
// Configuration parameters
protected bool $my_option = true;
// Hook called during form configuration
public function configure(object $builder): void
{
// Add fields, actions, HTML elements
}
// Hook called before form render
public function beforeRender(array $fields): array
{
// Modify fields before rendering
return $fields;
}
// Hook called before data is saved
public function beforeSave(array $request): array
{
// Modify request data
return $request;
}
// Hook called after data is saved
public function afterSave(array $request): void
{
// Execute custom logic
}
}
Accessing the Model
Get the model instance from the FormBuilder:
public function configure(object $builder): void
{
// Get the model
$model = $builder->getModel();
// Access model properties
$primary_key = $model->getPrimaryKey();
$table = $model->getTable();
}
Accessing Model Extensions
Get loaded model extensions to access their data and configuration:
public function configure(object $builder): void
{
$model = $builder->getModel();
// Get a specific extension
$soft_del_ext = $model->getLoadedExtension('SoftDelete');
if (!$soft_del_ext) {
return; // Extension not loaded
}
// Access extension properties
$field_name = $soft_del_ext->field_name;
// Check field value
if ($model->$field_name != null) {
// Record is soft deleted
}
}
Available Hooks
| Hook | Parameters | Return | Description |
|---|---|---|---|
configure() |
object $builder |
void |
Add fields, actions, HTML during form build |
beforeRender() |
array $fields |
array |
Modify fields array before render |
beforeSave() |
array $request |
array |
Modify request data before save |
afterSave() |
array $request |
void |
Execute logic after save |
Hook Details
configure(object $builder): void
When called: During form builder initialization, before fields are processed.
Input: The FormBuilder instance ($builder)
Use cases:
- Add new fields with
$builder->addField() - Add custom actions/buttons with
$builder->addActions() - Add HTML alerts or messages with
$builder->addHtml() - Access control checks (redirect unauthorized users)
- Show/hide UI elements based on record state
beforeRender(array $fields): array
When called: After fields are built but before rendering to HTML.
Input: Array of field definitions
Return: Modified array of field definitions
Use cases:
- Modify field properties (type, label, options, edit mode)
- Hide/show fields conditionally
- Change field types dynamically
- Adjust field permissions
beforeSave(array $request): array
When called: Before data is saved to the database.
Input: Request data array to be saved
Return: Modified request data array
Use cases:
- Validate ownership before save
- Auto-fill fields (author, timestamps)
- Remove fields that shouldn't be saved
- Transform data before save
- Deny unauthorized save attempts
afterSave(array $request): void
When called: After data has been successfully saved.
Input: The saved request data
Use cases:
- Log actions or create audit trail
- Send notifications
- Update related records
- Clear caches
- Trigger external APIs
FormBuilder Methods
Common methods available in the FormBuilder instance:
| Method | Parameters | Description |
|---|---|---|
getModel() |
- | Get the associated model instance |
getPage() |
- | Get the current page name |
addField() |
string $name, string $type, array $options |
Add a new form field |
addHtml() |
string $html |
Add custom HTML to the form |
addActions() |
array $actions |
Add custom action buttons |
Extension Parameters
Define configurable parameters as protected properties:
class FormBuilder extends AbstractFormBuilderExtension
{
// Default values
protected bool $show_author_info = true;
protected bool $show_restore_button = true;
}
// Override in module configuration
$rule_builder->addExtension('Author', [
'show_author_info' => false
]);
Example 1: SoftDelete FormBuilder Extension
namespace Extensions\SoftDelete;
class FormBuilder extends AbstractFormBuilderExtension
{
protected bool $show_restore_button = true;
public function configure(object $builder): void
{
$model = $builder->getModel();
$soft_del_ext = $model->getLoadedExtension('SoftDelete');
if (!$soft_del_ext) {
return;
}
$name = $soft_del_ext->field_name;
// Show alert for deleted records
if ($model->$name != null) {
$formatted = $model->getFormattedValue($name);
$builder->addHtml('<div class="alert alert-warning">
Deleted on <b>' . $formatted . '</b>
</div>');
}
// Add restore button
if ($model->$name != null) {
$builder->addActions([
'restore' => [
'label' => 'Restore',
'type' => 'submit',
'class' => 'btn btn-warning',
'action' => function($form_builder, $request) {
$model = $form_builder->getModel();
$soft_del_ext = $model->getLoadedExtension('SoftDelete');
$id = $request[$model->getPrimaryKey()] ?? 0;
$record = $model->getById($id);
if (!$record->isEmpty()) {
$record->{$soft_del_ext->field_name} = null;
$record->save();
return [
'success' => true,
'message' => 'Record restored'
];
}
return [
'success' => false,
'message' => 'Failed to restore'
];
}
]
]);
}
}
}
Example 2: Author Access Control
Prevents users from editing records they didn't create:
namespace Extensions\Author;
use App\Abstracts\AbstractFormBuilderExtension;
use App\{Get, Permissions, Route};
class FormBuilder extends AbstractFormBuilderExtension
{
protected bool $show_author_info = true;
public function configure(object $builder): void
{
Get::make('Auth');
$model = $builder->getModel();
$page = $builder->getPage();
// Check if user has "manage_own_only" permission
if ($page && Permissions::check($page . '.manage_own_only')) {
$user = Get::make('Auth')->getUser();
$current_user_id = $user->id ?? 0;
$primary_key = $model->getPrimaryKey();
$record_id = $model->$primary_key ?? 0;
// Check ownership for existing records
if ($record_id > 0) {
$created_by = $model->created_by ?? 0;
if ($created_by != $current_user_id) {
// Deny access
$builder->addHtml(
'<div class="alert alert-danger">
Access Denied: You can only edit your own records.
</div>'
);
$queryString = Route::getQueryString();
Route::redirect('?page=deny&redirect=' .
Route::urlsafeB64Encode($queryString));
}
}
}
}
public function beforeSave(array $request): array
{
Get::make('Auth');
// Skip for administrators
if (Permissions::check('_user.is_admin')) {
return $request;
}
$model = $this->builder->getModel();
$page = $this->builder->getPage();
if ($page && Permissions::check($page . '.manage_own_only')) {
$user = Get::make('Auth')->getUser();
$current_user_id = $user->id ?? 0;
// Auto-fill created_by for new records
$primary_key = $model->getPrimaryKey();
$record_id = $request[$primary_key] ?? 0;
if ($record_id == 0) {
$request['created_by'] = $current_user_id;
}
}
return $request;
}
}
Example 3: Adding Custom Action Buttons
Add a custom "Duplicate" button with action logic:
namespace Extensions\Duplicate;
use App\Abstracts\AbstractFormBuilderExtension;
class FormBuilder extends AbstractFormBuilderExtension
{
protected bool $show_duplicate_button = true;
public function configure(object $builder): void
{
if (!$this->show_duplicate_button) {
return;
}
$model = $builder->getModel();
$primary_key = $model->getPrimaryKey();
$record_id = $model->$primary_key ?? 0;
// Only show for existing records
if ($record_id > 0) {
$builder->addActions([
'duplicate' => [
'label' => 'Duplicate Record',
'type' => 'submit',
'class' => 'btn btn-info',
'action' => function($form_builder, $request) {
$model = $form_builder->getModel();
$pk = $model->getPrimaryKey();
$id = $request[$pk] ?? 0;
// Load original record
$record = $model->getById($id);
if ($record->isEmpty()) {
return [
'success' => false,
'message' => 'Record not found'
];
}
// Create duplicate
$duplicate = $model->createRow();
foreach ($record->toArray() as $key => $value) {
if ($key !== $pk) {
$duplicate->$key = $value;
}
}
$duplicate->save();
return [
'success' => true,
'message' => 'Record duplicated successfully',
'redirect' => '?page=' . $form_builder->getPage() .
'&action=edit&id=' . $duplicate->$pk
];
}
]
]);
}
}
}
Example 4: Conditional Field Visibility
Show/hide fields based on other field values:
namespace Extensions\ConditionalFields;
use App\Abstracts\AbstractFormBuilderExtension;
class FormBuilder extends AbstractFormBuilderExtension
{
public function beforeRender(array $fields): array
{
$model = $this->builder->getModel();
// Hide shipping fields if product is digital
if (isset($fields['product_type']) &&
$model->product_type === 'digital') {
if (isset($fields['weight'])) {
$fields['weight']['edit'] = false;
}
if (isset($fields['shipping_cost'])) {
$fields['shipping_cost']['edit'] = false;
}
}
// Show discount field only for admin users
if (isset($fields['discount'])) {
if (!Permissions::check('_user.is_admin')) {
$fields['discount']['edit'] = false;
}
}
return $fields;
}
}
Example 5: Data Transformation Before Save
Transform or validate data before saving:
namespace Extensions\DataTransform;
use App\Abstracts\AbstractFormBuilderExtension;
class FormBuilder extends AbstractFormBuilderExtension
{
public function beforeSave(array $request): array
{
// Normalize phone number format
if (isset($request['phone'])) {
$phone = preg_replace('/[^0-9]/', '', $request['phone']);
$request['phone'] = $phone;
}
// Generate slug from title
if (isset($request['title']) && empty($request['slug'])) {
$slug = strtolower($request['title']);
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
$request['slug'] = trim($slug, '-');
}
// Convert empty strings to null for numeric fields
$numeric_fields = ['price', 'quantity', 'discount'];
foreach ($numeric_fields as $field) {
if (isset($request[$field]) && $request[$field] === '') {
$request[$field] = null;
}
}
return $request;
}
}
Common Use Cases
- Conditional fields - Show/hide fields based on record state or user permissions
- Custom actions - Add buttons with custom logic (duplicate, restore, approve)
- Alerts and messages - Display contextual information (deleted status, warnings)
- Field modification - Change field properties, types, or options before render
- Access control - Prevent unauthorized users from editing specific records
- Data validation - Additional checks and validations before save
- Data transformation - Format, normalize, or enrich data before save
- Auto-fill fields - Automatically set values (author, timestamps, defaults)
- Integration with model extensions - React to extension data and state
Best Practices
- Use
configure()for adding UI elements and checking initial access control - Use
beforeRender()for modifying field properties and conditional visibility - Use
beforeSave()for data validation, transformation, and ownership checks - Use
afterSave()for side effects (logging, notifications, related updates) - Always check permissions before granting access or modifying data
- Use protected properties for configurable extension parameters
- Return modified arrays in
beforeRender()andbeforeSave() - Handle errors gracefully and provide clear user feedback
- Access the model via
$builder->getModel()to get record data