Milk Admin

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() and beforeSave()
  • Handle errors gracefully and provide clear user feedback
  • Access the model via $builder->getModel() to get record data

See Also

Loading...