Projects View Finalization

Within terminal, run the command:

apex create table disaster projects

Now open the newly generated file at /src/Disaster/Opus/DataTables/Projects.php and modify it as follows.

At the top within the use declarations, add the line:

use App\Disaster\Models\Location;

Define the columns the table will consist of. Within the properties, change the $columns array to:

public array $columns = [
    'id' => 'ID',
    'location' => 'Location',
    'title' => 'Title',
    'manage' => 'Manage'

Just under the $columns property there will be a $sortable array. Any column aliases within this awway will have ascending / descending clickable arrows within the column header. Add the "title" to this array, so it should be:

public array $sortable = ['id', 'title'];

Next, within the properties change the $has_search property to true. Doing so will add an AJAX powered search bar in the top right corner of the table. This line should read:

public bool $has_search = true;

Once done, within the __construct() method add the line:

$this->status = $attr['status'] ?? 'in_progress';

This will check for the "status" attribute within the <s:function> HTML tag that displays the data table, and if one doesn't exist will default to "in_progress".


Find the getTotal() method which is just under the __construct() method, and make the following changes:

  • Within the first SQL statement after if (search_term != '') change "some_column" to "title". When someone searches the data table, it will search the "title" column within the "disaster_projects" table for a comparison.
  • Within both SQL statements add WHERE status = %s, but naturally don't add the WHERE out of the first SQL statement as it's already there. Then add a parameter to each of $this->status.

At the end of modifications the getTotal() method should be:

public function getTotal(string $search_term = ''):int 

    // Get total
    if ($search_term != '') { 
        $total = $this->db->getField("SELECT count(*) FROM disaster_projects WHERE status = %s AND title LIKE %ls", $this->status, $search_term);
    } else { 
        $total = $this->db->getField("SELECT count(*) FROM disaster_projects WHERE status = %s", $this->status);

    // Return
    return (int) $total;

The getTotal() method is used to retrive the total number of rows within the data table to automatically generate the necessary pagination links.


Next you will see the getRows() method. At the beginning of this method it's going to run a SQL query to get the exact rows to display within the data table. Make the exact same changes as the getTotal() method. Once done, the method should be:

public function getRows(int $start = 0, string $search_term = '', string $order_by = 'id asc'):array 

    // Get rows
    if ($search_term != '') { 
        $rows = $this->db->query("SELECT * FROM disaster_projects WHERE status = %s AND title LIKE %ls ORDER BY $order_by LIMIT $start,$this->rows_per_page", $this->status, $search_term);
    } else { 
        $rows = $this->db->query("SELECT * FROM disaster_projects WHERE status = %s ORDER BY $order_by LIMIT $start,$this->rows_per_page", $this->status);

    // Go through rows
    $results = [];
    foreach ($rows as $row) { 
        $results[] = $this->formatRow($row);

    // Return
    return $results;

This method simply retrieves the appropriate rows to display within the data table. Below explains the parameters this method takes:

Variable Type Description
$start int Offset of the resulting rows to start from. This is automated, starts at 0, but changes when the AJAX based pagination menus are clicked.
$search_term string Automatically filled in and will only be not blank if the AJAX powered search bar is filled in.
$order_by string Default search order of the rows, which may change depending on whether or not the user clicks on the AJAX powered ascending / descending rows within the data table column headers. You may wish to change the default value such as for example, created_at DESC

Upon retrieving the rows to be displayed within the data table, this method goes through each row and passes it through the formatRow() column as explained below.


Next method that needs to be modified is the last method in the class called formatRow(). Modify it to be:

public function formatRow(array $row):array

    // Get location
    $loc = Location::whereId($row['location_id']);
    $row['location'] = $loc->getName();

    // Manage
    $row['manage'] = "<center><a href="~href~" class="btn btn-primary btn-md">Submit</a></center>";

    // Return
    return $row;

Within this method we obtained the location which includes the city and full country name, plus defined the Manage button that will be displayed in the right most column of the table.


Within the above section we modified the formatRow() method to call a Location::getName() method which doesn't currently exist. Open the file at /src/Disaster/Models/Location.php and at the top within the use declarations, add the line:

The CountryList class simply contains meta data on all countries within the world.

Next, add the following method to the file:

 * Get name
public function getName():string
    $name = $this->city . ', ' . CountryList::$opt[$this->country]['name'];
    return $name;

The above method simply formats the location to the city name and full country name instead of only a two letter abbreviation.


To tie everything together, open the file at /views/html/admin/disaster/projects.html and replace its contents with:



    <s:tab_page name="Create Project">
        <h3>Create New Project</h3>

            <s:ft_select name="location_id" label="Location" required="1">
            <s:ft_select name="status" value="in_progress" required="1" data_source="hash.disaster.project_status">
            <s:ft_textbox name="title">
            <s:ft_textarea name="description">
            <s:ft_submit value="create" label="Create New Project">

    <s:tab_page name="In Progress">
        <h3>In Progress</h3>
        <s:function alias="display_table" table="disaster.projects" status="in_progress" id="tbl_in_progress">

    <s:tab_page name="Pending">
        <s:function alias="display_table" table="disaster.projects" status="pending" id="tbl_pending">

    <s:tab_page name="Halted">
        <s:function alias="display_table" table="disaster.projects" status="halted" id="tbl_halted">

    <s:tab_page name="Completed">
        <s:function alias="display_table" table="disaster.projects" status="complete" id="tbl_complete">


We added four new tabs to the tab control, one for each status added in the hashes section section of the /etc/Disaster/package.yml file we previously created. Combine this with the line we added into the __construct() method within the data table class of:

$this->status = $attr['status'] ?? 'in_progress';

And you can see how data tables work. Any attributes you pass within the HTML tag will be available within the $attr array passed into the __construct() method of the data table class. This allows you to modify rows and columns within data tables as desired.


Open your web browser and visit the Disaster Relief->Projects menu of the administration panel again which will be located at You willl see a tab control with all five tab pages, one to create a project, and one for each available project status. Create some projects and everything will work exactly as expected.

Let's summarize everything we did in the last few pages:

  • Created a new view at admin/disaster/projects to manage projects.
  • Every view within Apex has both, a .html and accompanying .php file.
  • The .php files of every view allow for the render() method which is always executed. Plus they also allow for HTTP request method named methods such as post(), get(), delete() and so on. Full information can be found on the PHP Class per-view page of the developer documentation.
  • All methods within the .php file for a view support method based injection, hence things such as the ProjectController being injected as shown in the the previous page.
  • The /src/Disaster/ directory starts as a clean slate and is your blank canvas to develop in any design and style you prefer.
  • The ProjectController class above allows basic CRUD operations while utilizing the Project model class we previously created. Full information can be found on the models section of the documentation.
  • The Location model was modified and a getName() method was added.