Trongate Way Docs

Building the Controller

The controller handles all incoming HTTP requests, enforces authentication, delegates data operations to the model, and renders views. In Trongate v2, controllers extend the Trongate base class and are stored in the module directory as Countries.php.

The Complete Controller

PHP
<?php
/**
 * Countries Controller
 *
 * Manages country records with full CRUD operations.
 */
class Countries extends Trongate {

    private int $default_limit = 20;
    private array $per_page_options = [10, 20, 50, 100];
    
    /**
     * Default entry point - redirects to manage page
     *
     * @return void
     */
    public function index(): void {
        redirect('countries/manage');
    }

    /**
     * Display paginated list of countries.
     *
     * Shows records in a table with pagination controls. Includes
     * dropdown for selecting number of records per page.
     *
     * @return void
     */
    public function manage(): void {
        $this->trongate_security->make_sure_allowed();

        $search_query = $_GET['search_query'] ?? '';
        $search_column = $_GET['search_column'] ?? '';

        // Validate column against known searchable columns.
        $allowed_columns = $this->model->get_searchable_columns();
        if ($search_column !== '' && !in_array($search_column, $allowed_columns, true)) {
            $search_column = '';
        }

        $search_active = ($search_query !== '');

        if ($search_active) {
            $total_rows = $this->model->count_search_results($search_query, $search_column);
        } else {
            $total_rows = $this->model->count_all();
        }
        
        $limit = $this->get_limit();
        $offset = $this->get_offset();

        if ($search_active) {
            $rows = $this->model->search_records($search_query, $search_column, $limit, $offset);
        } else {
            $rows = $this->model->fetch_records($limit, $offset);
        }
        $rows = $this->model->prepare_records_for_display($rows);

        $data = [
            'rows' => $rows,
            'pagination_data' => $this->get_pagination_data($total_rows, $limit, $search_query, $search_column),
            'view_module' => 'countries',
            'view_file' => 'manage',
            'per_page_options' => $this->per_page_options,
            'selected_per_page' => $this->get_selected_per_page()
        ];

        $data['search_query'] = $search_query;
        $data['search_column'] = $search_column;
        $data['search_active'] = $search_active;
        $this->templates->admin($data);
    }

    /**
     * Display form for creating or editing a country.
     *
     * Shows form with appropriate headline and action URL.
     * Automatically repopulates form with submitted data on validation errors.
     *
     * @return void
     */
    public function create(): void {
        $this->trongate_security->make_sure_allowed();

        $update_id = segment(3, 'int');

        if ((REQUEST_TYPE === 'GET') && ($update_id > 0)) {
            $data = $this->model->get_data_from_db($update_id);
        } else {
            $data = $this->model->get_data_from_post();
        }

        // Add view-specific data
        $data['headline'] = ($update_id > 0) ? 'Update Country Record' : 'Create New Country Record';
        $data['cancel_url'] = ($update_id > 0) ? 'countries/show/'.$update_id : 'countries/manage';
        $data['form_location'] = 'countries/submit/'.$update_id;
        $data['view_module'] = 'countries';
        $data['view_file'] = 'create';
        $this->templates->admin($data);
    }

    /**
     * Handle form submission for creating/updating countries.
     *
     * Validates input, converts checkbox data, and saves to database.
     * Includes automatic CSRF validation and proper checkbox conversion.
     *
     * @return void
     */
    public function submit(): void {
        $this->trongate_security->make_sure_allowed();

        $submit = post('submit', true);

        if ($submit === 'Submit') {
            $this->validation->set_rules('country_title', 'country title', 'required|min_length[2]|max_length[255]');
            $this->validation->set_rules('country_code', 'country code', 'required|min_length[2]|max_length[2]');

            if ($this->validation->run()) {
                $update_id = segment(3, 'int');
                $data = $this->model->get_data_from_post();

                if ($update_id > 0) {
                    $this->model->update_record($update_id, $data);
                    $flash_msg = 'Country updated successfully';
                    $finish_url = 'countries/show/'.$update_id;
                } else {
                    $update_id = $this->model->create_new_record($data);
                    $flash_msg = 'Country created successfully';
                    $finish_url = 'countries/manage';
                }

                set_flashdata($flash_msg);
                redirect($finish_url);
            } else {
                $this->create();
            }
        } else {
            redirect('countries/manage');
        }
    }

    /**
     * Display detailed view of a single country.
     *
     * Shows all details with edit/delete options.
     * Automatically handles missing records with 404 page.
     *
     * @return void
     */
    public function show(): void {
        $this->trongate_security->make_sure_allowed();

        $update_id = segment(3, 'int');

        if ($update_id === 0) {
            redirect('countries/manage');
        }

        // Fetch record and prepare for display.
        $data = $this->model->get_data_from_db($update_id, true);

        if ($data === false) {
            $this->not_found();
            return;
        }

        // Add additional view data
        $data['update_id'] = $update_id;
        $data['headline'] = 'Country Details';
        $data['back_url'] = $this->get_back_url();
        $data['view_module'] = 'countries';
        $data['view_file'] = 'show';
        $this->templates->admin($data);
    }

    /**
     * Display confirmation page before deleting a country.
     *
     * Shows confirmation dialog with details to prevent accidental deletion.
     *
     * @return void
     */
    public function delete_conf(): void {
        $this->trongate_security->make_sure_allowed();

        $update_id = segment(3, 'int');

        if ($update_id === 0) {
            $this->not_found();
            return;
        }

        $data = $this->model->get_data_for_edit($update_id);

        if ($data === false) {
            $this->not_found();
            return;
        }

        $data['update_id'] = $update_id;
        $data['headline'] = 'Delete Country Record';
        $data['cancel_url'] = 'countries/show/'.$update_id;
        $data['form_location'] = 'countries/submit_delete/'.$update_id;
        $data['view_module'] = 'countries';
        $data['view_file'] = 'delete_conf';
        $this->templates->admin($data);
    }

    /**
     * Handle country deletion after confirmation.
     *
     * Verifies confirmation and deletes record from database.
     * Includes safety checks to prevent unauthorized deletion.
     *
     * @return void
     */
    public function submit_delete(): void {
        $this->trongate_security->make_sure_allowed();

        $submit = post('submit', true);

        if ($submit === 'Yes - Delete Now') {
            $update_id = segment(3, 'int');

            if ($update_id === 0) {
                redirect('countries/manage');
                return;
            }

            $record = $this->model->find_by_id($update_id);

            if ($record === false) {
                redirect('countries/manage');
                return;
            }

            $this->model->delete_record($update_id);

            set_flashdata('The record was successfully deleted');
            redirect('countries/manage');
        } else {
            redirect('countries/manage');
        }
    }

    /**
     * Set number of records per page for pagination.
     *
     * Stores user preference in session for consistent pagination across requests.
     *
     * @return void
     */
    public function set_per_page(): void {
        $this->trongate_security->make_sure_allowed();

        $selected_index = segment(3, 'int');

        if (!isset($this->per_page_options[$selected_index])) {
            $selected_index = 1;
        }

        $_SESSION['selected_per_page'] = $selected_index;
        redirect('countries/manage');
    }

    /**
     * Generate pagination configuration data.
     *
     * @param int    $total_rows    Total number of records
     * @param int    $limit         Number of records per page
     * @param string $search_query  The search query string
     * @param string $search_column The column being searched
     * @return array Pagination configuration for template
     */
    private function get_pagination_data(int $total_rows, int $limit, string $search_query = '', string $search_column = ''): array {
        $pagination_query = '';
        if ($search_query !== '' && $search_column !== '') {
            $pagination_query = 'search_query=' . urlencode($search_query) . '&search_column=' . urlencode($search_column);
        }

        return [
            'total_rows' => $total_rows,
            'limit' => $limit,
            'pagination_root' => 'countries/manage',
            'pagination_query' => $pagination_query,
            'record_name_plural' => 'countries',
            'include_showing_statement' => true
        ];
    }

    /**
     * Determine appropriate back URL for navigation.
     *
     * Uses previous URL if it was the manage page, otherwise defaults to manage.
     *
     * @return string URL for back button
     */
    private function get_back_url(): string {
        $previous_url = previous_url();
        if ($previous_url !== '' && strpos($previous_url, BASE_URL . 'countries/manage') === 0) {
            return $previous_url;
        }
        return BASE_URL . 'countries/manage';
    }

    /**
     * Display 404-style not found page for missing countries.
     *
     * Shows user-friendly error message with navigation back.
     *
     * @return void
     */
    private function not_found(): void {
        $data = [
            'headline' => 'Country Not Found',
            'message' => 'The country you\'re looking for doesn\'t exist or has been deleted.',
            'back_url' => $this->get_back_url(),
            'back_label' => 'Go Back',
            'view_module' => 'countries',
            'view_file' => 'not_found'
        ];
        $this->templates->admin($data);
    }

    /**
     * Get selected per-page index from session.
     *
     * @return int Index of selected per-page option
     */
    private function get_selected_per_page(): int {
        return $_SESSION['selected_per_page'] ?? 1;
    }

    /**
     * Get current pagination limit from session.
     *
     * @return int Number of records to display per page
     */
    private function get_limit(): int {
        if (isset($_SESSION['selected_per_page'])) {
            return $this->per_page_options[$_SESSION['selected_per_page']];
        }
        return $this->default_limit;
    }

    /**
     * Calculate pagination offset based on page number.
     *
     * @return int Database offset for current page
     */
    private function get_offset(): int {
        $page_num = segment(3, 'int');
        return ($page_num > 1) ? ($page_num - 1) * $this->get_limit() : 0;
    }

    /**
     * Handle search form submission for filtering countries.
     *
     * Preprocesses text parameters and triggers custom query filtering.
     *
     * @return void
     */
    public function submit_search(): void {
        $this->trongate_security->make_sure_allowed();

        $search_query = post('search_query', true);
        $search_column = post('search_column', true);

        // Validate column against known searchable columns.
        $allowed_columns = $this->model->get_searchable_columns();
        if ($search_column !== '' && !in_array($search_column, $allowed_columns, true)) {
            $search_column = '';
        }

        // Preprocess the query
        $search_query = trim($search_query);
        $search_query = preg_replace('/\s+/', ' ', $search_query);

        // Validate minimum 2 character length
        if (strlen($search_query) < 2) {
            set_flashdata('Search query must be at least 2 characters');
            redirect('countries/manage');
        }

        redirect('countries/manage?search_query=' . urlencode($search_query) . '&search_column=' . urlencode($search_column));
    }

    /**
     * Display the search modal form for filtering countries.
     *
     * Renders a form with a search input and a dropdown of searchable columns.
     *
     * @return void
     */
    public function search_modal(): void {
        $this->trongate_security->make_sure_allowed();

        $data['view_module'] = 'countries';
        $data['view_file'] = 'search_modal';
        $this->view('search_modal', $data);
    }


}

Properties

$default_limit

Sets the default number of records per page (20) when the user has not yet chosen a custom value. This is used by the get_limit() private helper as a fallback when no per-page preference has been stored in the session.

$per_page_options

Defines the available options for the records-per-page dropdown: 10, 20, 50, 100. The user's selection is stored in the session via the set_per_page() method and persists across requests.

Method by Method

index()

The default entry point for the module. It simply redirects to countries/manage, ensuring that visiting the module root takes the user to the paginated list rather than showing an empty page.

manage()

The main list page. This method coordinates paginated data retrieval, search filtering, and view rendering. Key flow:

  • Reads optional search_query and search_column from the query string.
  • Validates the requested column against get_searchable_columns() to prevent SQL injection.
  • Counts total rows (either all records or filtered by search).
  • Calculates $limit and $offset using private helpers that read from the URL segment and session.
  • Fetches records and passes them through prepare_records_for_display().
  • Renders the manage.php view inside the admin template.

create()

Handles both the create and edit forms in a single method. When the request is a GET with an update_id present, it fetches the existing record from the database for editing. Otherwise it uses get_data_from_post() to start with empty form fields (or previously submitted values after a validation failure). The headline, cancel URL, and form action are all set based on whether an update_id is present.

submit()

Processes form submissions. On a valid "Submit" button press, it sets validation rules, runs validation, and either inserts or updates the record. On success, it sets a flashdata message and redirects appropriately. On validation failure, it calls $this->create() to re-render the form with error messages.

show()

Displays a single record in a read-only detail view. It calls get_data_from_db($update_id, true) to fetch and prepare the record for display. If the record is not found, it calls the private not_found() method. The $back_url is determined by get_back_url(), which intelligently returns the user to the manage page if that was their previous stop.

delete_conf()

The first step of the two-step delete flow. It fetches the record via get_data_for_edit() (raw data, no display transformations) to confirm it exists, then renders a confirmation view. The Cancel button returns to the show page for that record.

submit_delete()

The second step of the delete flow. It checks that the submit button value is exactly 'Yes - Delete Now' (preventing direct URL access), verifies the record still exists, deletes it, sets a flashdata message, and redirects to the manage page.

set_per_page()

Stores the user's preferred records-per-page setting in the session. The selected index is read from URL segment 3 and validated against the $per_page_options array before saving.

Private Helpers

get_pagination_data()

Builds the configuration array required by the pagination module, including total rows, current limit, the pagination root URL, and optional search parameters. This data is passed to Modules::run('pagination/display', ...) in the manage view.

get_back_url()

Determines the URL for the "Back" button on the show page. If the previous URL in the browser history was the manage page, it returns that exact URL. Otherwise it defaults to /countries/manage.

not_found()

A private method that renders a user-friendly "not found" page. Because it is private (not prefixed with block_url()), it cannot be called directly as a URL endpoint.

get_selected_per_page(), get_limit(), get_offset()

These three helpers work together to manage pagination state. get_selected_per_page() reads the user's preference from the session. get_limit() returns the corresponding value from $per_page_options (or the default if no preference is set). get_offset() calculates the database offset based on the current page number.

submit_search()

Processes the search form submission. It reads the search query and column from POST data, validates the column against the whitelist, trims and normalises the query, enforces a minimum length of 2 characters, and redirects back to the manage page with the search parameters in the query string.

search_modal()

Renders the search modal form as a standalone view (without the admin template wrapper). This is loaded via MX into a modal overlay when the user clicks the Search button on the manage page.

This controller demonstrates the complete CRUD pattern used by every admin module in Trongate: authentication on every public method, delegation to the model for data operations, and clean separation of view logic.

We're continually improving the Trongate documentation. If anything is incorrect, unclear, incomplete, or could be better, we'd genuinely appreciate your input.

Share your thoughts in the Documentation Feedback.

Leave Feedback About This Page