The Create/Update Pattern
Building forms for creating and editing records is a universal requirement in web applications. Trongate v2 encourages an elegant pattern that handles both operations with a single form, single set of validation rules, and minimal code duplication.
Why This Pattern Works
- DRY (Don't Repeat Yourself) — One form, one view, one set of validation rules
- Simple Logic — A single conditional determines data source
- Maintainable — Change fields in one place, not two
- User-Friendly — Consistent interface for creation and editing
The Three Components
| Component |
Purpose |
Key Responsibility |
create() |
Display the form |
Decide whether to load data from POST or database |
submit() |
Process submission |
Validate, save, redirect on success or redisplay on failure |
| Model helpers |
Data preparation |
get_data_from_post() and get_data_from_db() |
Complete Working Example
Here's a complete Members module demonstrating the pattern:
The Controller (Members.php)
<?php
class Members extends Trongate {
/**
* Display a list of members.
*/
public function manage(): void {
$this->trongate_security->make_sure_allowed();
$data = [
'members' => $this->model->fetch_members(),
'view_module' => 'members',
'view_file' => 'manage'
];
$this->templates->admin($data);
}
/**
* Display the create or update member form.
*/
public function create(): void {
$this->trongate_security->make_sure_allowed();
$update_id = segment(3, 'int');
$submit = post('submit');
// Load data from POST if creating new OR if validation failed
// Otherwise load from database for editing
if (($update_id === 0) || ($submit === 'Submit')) {
$data = $this->model->get_data_from_post();
} else {
$data = $this->model->get_data_from_db($update_id);
}
$data['headline'] = ($update_id === 0) ? 'Create Member' : 'Update Member';
$data['update_id'] = $update_id;
$data['form_location'] = str_replace('/create', '/submit', current_url());
$data['view_module'] = 'members';
$data['view_file'] = 'create';
$this->templates->admin($data);
}
/**
* Handle form submission for creating or updating a member.
*/
public function submit(): void {
$this->trongate_security->make_sure_allowed();
// Validate the submitted data
$this->validation->set_rules('username', 'username', 'required|min_length[3]|max_length[30]');
$this->validation->set_rules('email_address', 'email address', 'required|valid_email');
$result = $this->validation->run();
if ($result === true) {
// Validation passed - save data and redirect
$data = $this->model->get_data_from_post();
$update_id = segment(3, 'int');
if ($update_id === 0) {
$this->db->insert($data, 'members');
set_flashdata('The new member was successfully created.');
} else {
$this->db->update($update_id, $data, 'members');
set_flashdata('The member was successfully updated.');
}
redirect('members/manage');
} else {
// Validation failed - redisplay form with errors
$this->create();
}
}
/**
* Display the delete confirmation screen.
*/
public function confirm_delete(): void {
$this->trongate_security->make_sure_allowed();
$update_id = segment(3, 'int');
$this->model->get_data_from_db($update_id);
$data = [
'form_location' => str_replace('/confirm_delete', '/submit_confirm_delete', current_url()),
'update_id' => $update_id,
'view_module' => 'members',
'view_file' => 'confirm_delete'
];
$this->templates->admin($data);
}
/**
* Handle confirmed deletion of a member.
*/
public function submit_confirm_delete(): void {
$this->trongate_security->make_sure_allowed();
$update_id = (int) post('update_id', true);
$this->db->delete($update_id, 'members');
set_flashdata('The member record was successfully deleted.');
redirect('members/manage');
}
}
The Model (Members_model.php)
<?php
class Members_model extends Model {
/**
* Retrieve and sanitise member data from POST.
*
* Used for both saving to database and redisplaying form after validation errors.
*
* @return array
*/
public function get_data_from_post(): array {
$data = [
'username' => post('username', true),
'email_address' => post('email_address', true),
'active' => (int) (bool) post('active', true)
];
return $data;
}
/**
* Retrieve a single member record from the database.
*
* Used when editing an existing member.
*
* @param int $update_id
* @return array
*/
public function get_data_from_db(int $update_id): array {
$record_obj = $this->db->get_where($update_id, 'members');
if ($record_obj === false) {
http_response_code(404);
echo 'Member not found';
die();
}
$member = (array) $record_obj;
return $member;
}
/**
* Fetch all members and append a human-readable status.
*
* @return array
*/
public function fetch_members(): array {
$members = $this->db->get('id', 'members');
foreach ($members as $key => $member) {
$active = (int) $member->active;
$members[$key]->status = ($active === 1) ? 'active' : 'inactive';
}
return $members;
}
}
The View (create.php)
<h1><?= $headline ?></h1>
<div class="card">
<div class="card-heading">Member Details</div>
<div class="card-body">
<?php
echo form_open($form_location, array('class' => 'highlight-errors'));
echo form_label('Username');
echo validation_errors('username');
echo form_input('username', $username, array('placeholder' => 'Username...', 'autocomplete' => 'off'));
echo form_label('Email Address');
echo validation_errors('email_address');
echo form_email('email_address', $email_address, array('placeholder' => '
[email protected]'));
echo '<label>';
echo form_checkbox('active', 1, $active);
echo ' Active member';
echo '</label>';
echo '<div class="text-center">';
echo anchor('members/manage', 'Cancel', array('class' => 'button alt'));
if ($update_id > 0) {
echo anchor('members/confirm_delete/'.$update_id, 'Delete Member', array('class' => 'button danger'));
}
echo form_submit('submit', 'Submit');
echo '</div>';
echo form_close();
?>
</div>
</div>
How It Works: The Four Scenarios
The key to this pattern is the conditional in create():
if (($update_id === 0) || ($submit === 'Submit')) {
$data = $this->model->get_data_from_post();
} else {
$data = $this->model->get_data_from_db($update_id);
}
Translation: "If we're creating a new record OR if the form was just submitted (validation error), get data from POST. Otherwise, we're editing an existing record, so get data from the database."
Scenario 1: Creating a New Record
| URL | /members/create |
| Condition | $update_id === 0 is true |
| Data Source | get_data_from_post() returns empty strings |
| Result | Empty form displays |
Scenario 2: Editing an Existing Record
| URL | /members/create/5 |
| Condition | $submit is empty (GET request) |
| Data Source | get_data_from_db(5) |
| Result | Form displays with existing data |
Scenario 3: Validation Error
| Trigger | User submits form with invalid data |
| Flow | submit() calls create() on failure |
| Condition | $submit === 'Submit' is true |
| Data Source | get_data_from_post() with submitted values |
| Result | Form redisplays with user's values and error messages |
Scenario 4: Successful Submission
| Trigger | User submits valid form |
| Validation | Passes in submit() |
| Database | Insert or update via get_data_from_post() |
| Feedback | set_flashdata() stores success message |
| Result | Redirect to manage page (PRG pattern) |
Validation Flow
Validation happens in submit(), not create():
- User submits form to
submit()
- set_rules() defines validation requirements
- included-validation-run() executes validation
- If valid: Save data, then invoke set_flashdata(), followed by redirect()
- If invalid: Errors stored in session,
create() gets called to redisplay form
Key Point: The create() method never validates. It only decides where to get data from (POST or database) and displays the form. Validation is exclusively the responsibility of submit().
Checkbox Handling
Checkboxes require special attention because unchecked boxes submit nothing. The following model code demonstrates a robust and consistent way to handle checkboxes:
public function get_data_from_post(): array {
$data = [
'username' => post('username', true),
'email_address' => post('email_address', true),
'active' => (int) (bool) post('active', true) // 0 or 1
];
return $data;
}
This conversion works for both:
- Database storage: Stores
0 or 1 in the database
- Form redisplay: Returns
0 or 1, which form_checkbox() interprets correctly
The Post-Redirect-Get (PRG) Pattern
After successful submission, Trongate uses PRG to prevent duplicate submissions:
| Step |
Action |
Result |
| POST | Form submits to submit() | Data received |
| Validate | run() checks data | Pass or fail |
| Save | Database insert or update | Record stored |
| Flashdata | set_flashdata() | Message stored in session |
| Redirect | redirect('members/manage') | Browser receives 302 |
| GET | Browser requests manage page | Success message displays |
If the user refreshes, they see the manage page (GET) — no resubmission warning.
URL Structure
| URL | Method | Action |
/members/manage | manage() | List all records |
/members/create | create() | New record form (empty) |
/members/create/5 | create() | Edit record form (ID 5) |
/members/submit | submit() | Create new record |
/members/submit/5 | submit() | Update record ID 5 |
/members/confirm_delete/5 | confirm_delete() | Confirm delete ID 5 |
- Two model methods only:
get_data_from_post() and get_data_from_db() handle all data loading
- Single validation location: Always validate in
submit(), never in create()
- Consistent checkbox handling: Use
(int) (bool) post('field', true) for all checkboxes
- Flashdata for success: Always use
set_flashdata() before redirecting
- Always redirect after POST: Prevents duplicate submissions on refresh
- URL conventions: Use
/create for forms, /submit for processing