1

I'm using Codeigniter 3 and the table library in order to display some data in the following format;

+---------------+---------------------+
| id            | 102                 |
+---------------+---------------------+
| First Name    | Ross                |
+---------------+---------------------+
| Last Name     | Bing                |
+---------------+---------------------+
| Title         | Doctor              |
+---------------+---------------------+
| Timestamp     | 2019-01-18 10:17:05 |
+---------------+---------------------+
| Member Number |                     |
+---------------+---------------------+

A vardump of $tableData is;

Array
(
    [0] => Array
        (
            [id] => 102
            [firstname] => Ross
            [lastname] => Bing
            [title] => Doctor
            [timestamp] => 2019-01-18 10:17:05
            [member_no] => 
        )
)

The PHP code I use to generate the HTML table is;

$tableData = $this->My_model->getData();

$heading = array(
    'id' => 'ID',
    'firstname' => 'First Name',
    'lastname' => 'Last Name',
    'title' => 'Title',
    'timestamp' => 'Date Submitted',
    'member_no' => 'Member Number'
);

$fields = array_keys($tableData[0]);
$rows = array();

foreach($fields as $key => $field) {
    $rows[$key][0] = array(
        'data' => '<strong>' . $heading[$field] . '</strong>'
    );
    foreach($tableData as $key2 => $item) {
        $rows[$key][$key2 + 1] = $item[$field];
    }
}

foreach($rows as $row) {
    $this->table->add_row($row);
}

The above code works fine, however if a row is empty (see member_no above) i'd like to do one of two things (whichever is easiest);

  • hide the table row completely
  • display not available in the table cell

How can I achieve this?

3
  • something like: $rows[$key][$key2 + 1] = ($item[$field!=''])?$item[$field]:'not available' Commented Feb 28, 2019 at 16:54
  • I find these 3 foreach loops to be ugly. Sorry but they are. You could probably do some array_intersect_key and array_merge magic to get rid of a few of those. Commented Feb 28, 2019 at 16:56
  • I would need an example of the input and output data, such as what $tableData looks like and what $rows looks like. An example here is when you use array_merge with string keys, the data in the second array replaces the data in the first, but the order of the keys in the first array is preserved. You could also array_filter the input data, removing any empty values, then pre-populate an array (with not available) with the keys matched to the $header array, then when you merge them anything that was removed from the filter retains the default value... etc. Commented Feb 28, 2019 at 16:58

1 Answer 1

2

I would do something like this:

$tableData = array (
    0 => 
    array (
        'id' => 102,
        'lastname' => 'Bing',
        'title' => 'Doctor',
        'timestamp' => '2019-01-1810:17:05',
        'member_no' => null,
        'firstname' => 'Ross', //intentionally moved to show ordering
        'foobar' => 'blah' //added for example, this will be removed by array_intersect_key
    ),
);

$heading = array(
    'id' => '<strong>ID</strong>',
    'firstname' => '<strong>First Name</strong>',
    'lastname' => '<strong>Last Name</strong>',
    'title' => '<strong>Title</strong>',
    'timestamp' => '<strong>Date Submitted</strong>',
    'member_no' => '<strong>Member Number</strong>'
);

//create a default array
//this takes the keys from $heading, and makes an array with all the values as 'not available'
// ['id' => 'not available','lastname' => 'not available', ... ]
$default = array_fill_keys(array_keys($heading), 'not available');
$rows = [];

foreach($tableData as $key => $row) {
    //remove all elements with strlen of 0 (in this case 'member_no')
    $row = array_filter($row, function($item){return strlen($item);});

    //removes 'foobar' or anything that has a key not in $heading
    $row = array_intersect_key($row, $heading);

    //combine $default and $data (empty items in $row are filled in from default)
    //such as items removed by array_filter above
    //the key order will match $default, which matches $headings
    $row = array_merge($default, $row);

    $rows[] = $row;
}

foreach($heading as $key=>$value) {
    print_r(array_merge([['data'=>$value]], array_column($rows, $key)));
}

Output

Array
(
    [0] => Array
        (
            [data] => <strong>ID</strong>
        )

    [1] => 102
    //[2] => 108
    //...
)
 ....

Sandbox

I kept these separate so it would be a bit easier to read, but there is no reason you cannot do it this way.

//...
$default = array_fill_keys(array_keys($heading), 'not available');

foreach($tableData as $key => $row) $rows[] = array_merge($default, array_intersect_key(array_filter($row, function($item){return strlen($item);}), $heading));

foreach($heading as $key=>$value) print_r(array_merge([['data'=>$value]],array_column($rows, $key)));

Sandbox

I had to guess a bit on what the end result was, so I ran your original code and it gave me this:

Array
(
    [0] => Array
        (
            [data] => <strong>ID</strong>
        )

    [1] => 102
    //[2] => 108
    //...
)
....

In my code you can replace the print_r with this call $this->table->add_row([..array data..]);. Where array data is the stuff in the print_r call. You could make this a variable, but what's the point if its only used here. That eliminates a few of those loops (see below) and A few other advantages:

  • key order of $headings is preserved, elements appear where they do in the $headings array, regardless of where they are in $tableData. This allows easy re-ording of the data, too, for example: you could even map this to a dynamic array, I do this in CSV files, which allows users to change the order of the headers and columns. They can even rename the headers, because the way the key => value pairing works my_key => their_key...
  • Data missing from $tableData is defaulted to not available pulled in from $default, in theory you could map this manually to different things. For example: you could default timestamp to the current time by doing $default['timestamp'] = date('Y-m-d H:i:s'); right after creating it with array_fill_keys.
  • Extra data in $tableData not defined in $headings is removed. Which is good for forward compatibility.
  • And it's A bit easier to make sense of (once you know how it works) because there are less "transient" variables and less fiddling with the keys ect...

Basically what I am doing is giving control over to the $headings array, in your original code. You do this somewhat by looping over the keys (fields), but this complicates things later like this $rows[$key][$key2 + 1]. It leaves you open to undefined array index issues, if the data changes at a later time, such as adding a new field to the DB. The order of the output is dependent on the data in $tableData which is less intuitive (and less useful) then if it depends on $headings.

Here is an example of these issues with the original code:

//for example if your data changes to this and run your original code
 $tableData = array (
 0 => 
    array (
        'id' => 102,
        'lastname' => 'Bing',
        'title' => 'Doctor',
        'timestamp' => '2019-01-1810:17:05',
        'member_no' => null,
        'firstname' => 'Ross', //intentionally moved to show ordering
        'foo' => 'bar' //added this undefined in $headings
    ),
);

You'll get this notice and also find the last 2 elements are:

 <br />
 <b>Notice</b>:  Undefined index: foo in <b>[...][...]</b> on line <b>30</b><br />
 //... all other elements ...

 //in orignal: displayed in the order of $tableData
 //in my code: order is based on $headings, so this would be moved to the correct location
Array(
    [0] => Array (
         [data] => <strong>First Name</strong>
    )        
    [1] => Ross
)

//in orignal: $headings['foo'] is not defined so we have no label here
//in my code: this element is removed and doesn't get rendered
Array(
     [0] => Array(
           [data] => <strong></strong>
      )

     [1] => bar
)

Sandbox (Orignal Code)

While these things may never cause an issue, it highlights my point about basing the output off of $headings and not $tableData. Things tend to change, and this way if you add/remove a field from this data, you wont have to worry about it breaking this page etc...

The combination of array_fill_keys, array_intersect_key and array_merge can be used to map the headers (As I shown above) of one array to another. You can use array_combine($headings, $row) to physically swap them and you would get something like this:

[
     [
         '<strong>ID</strong>' => 102,
         '<strong>First Name</strong>' => 'Ross',
          //...
     ],
     [...]
]

It works great for CSV files (which is what I figured it out on) and anything else you need to remap the keys for.

Anyway, hope it helps you!

Sign up to request clarification or add additional context in comments.

2 Comments

Hey @ArtisticPhoenix - this is one of the best answers on SO, it definitely helps me! Thanks very much for the detailed explanation, it all makes much more sense now!
It's a bit more complicated if you don't fully understand the functions that I use in this and how they work together. But it works really nice, I do a lot with CSV files that our users submit (we run them against a DB, and return a report). So we had to make a way to customize the headers (names, order ) etc... and I came up with the basic method of what I did above because of that... Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.