4

So I want to fill and display a HTML table, that is 2 columns wide. But, no matter what I try, my data always sticks together.

The thing is, that I actually want to put the entire data into a table, where it's sorted by the starting letter.

This is my code:

<?php include_once 'Database/dbh.php'; ?>

<!DOCTYPE html>
<html>
    <body>
        <?php
        $fetch = "SELECT DISTINCT Kategori, LEFT(Kategori, 1) AS Letter FROM kategorier ORDER BY Kategori;";
        $result = mysqli_query($conn, $fetch);
        $resultCheck = mysqli_num_rows($result);

        if ($resultCheck > 0) {
            while ($row = mysqli_fetch_assoc($result)) {
                if (!isset($lastLetter) || $lastLetter != $row['Letter']) {
                    $lastLetter = $row['Letter'];
                    echo "<h2>", $row['Letter'], "</h2>";
                }
                echo "<table><tr><td>" . $row['Kategori'] . "</td></tr></table>";
            }
        }
        ?>      
    </body>
</html>

Here is a picture of how it is right now:

Here is a picture of how it is right now


Here is how I want it to look like:

Here is how I want it to look like

dakis
  • 225
  • 1
  • 6
  • 32
MrSuckAlot
  • 43
  • 3
  • 5
    You're creating a whole new table on each iteration of the loop. Decide how you want the table designed, then break up the html within your script accordingly. – aynber Oct 05 '18 at 17:24
  • I dont think I understand you @aynber. So you want me to first make the table and then put the data into it? If that is the case, I already have tried that, but it doesnt seem to work. – MrSuckAlot Oct 05 '18 at 17:42
  • That's sort of correct. You want to start the table first, then create cells in the loop, then finish the table afterwards. If you want to have a table for each letter, then put in a check to stop/start the table when the letter changes (within your current if check). If you want to have a certain amount of columns per table (ie 5 words would be across 4 cells in 2 rows), then add a counter to close and start a row when it hits a certain number. – aynber Oct 05 '18 at 17:51

4 Answers4

1

I am sorry to disappoint you, but the solution is a bit more complex than you maybe thought. As I see, @aynber already suggested it. So, if he writes an answer, it would be fair to take his answer into consideration first.

Suggestions

About Separation of Concerns:

Let me now begin by saying, that, for the future, you should familiarize yourself with the Separation of Concerns principle. In simple words, taking your code as example: always separate the code involving access to database (for fetching data, updating, etc) from the one displaying the data (e.g. the HTML part of the page).

This means that, if you need to fetch data from the database, then do it at the top of the web page, and save it in arrays. Then just use these arrays inside the HTML part of the web page, instead of some db-related functions like mysqli_query, or mysqli_fetch_assoc, or etc. For clarity, see the HTML part of the code I provided ("index.php").

A big advantage of this approach is, that you can move the whole php code from the top of the page into php functions, or class methods. The arrays will then just contain data resulting from calling these functions/methods.

The main point for all of the above statements? Juggle as you wish with the php code and the data on top of the web page, and save the results in php arrays. The arrays should, in the end, have such a structure, that the job of the HTML part of the web page will be totally simple: to just read and display the array elements.

So don't mix HTML code with db-related code. If you do it, then the code is too hard to maintain.

About printing client-side code from PHP:

Another important convention you should remember for the future is, to not print any client-side code by using php code. E.g. to not use such statements like echo "<table><tr><td>".... In this echo case, just save the content that you want to present into variables, and display them in the HTML part of the web page as desired.

About prepared statements:

If you need to execute sql statements with parameters, then use prepared statements (instead of mysqli::query in this case). They will protect your code against eventual sql injections. For completion, at the end of this answer I posted an example of index.php using prepared statements instead of mysqli::query.

Solution to question:

Steps:

Regarding the solution I prepared, it involves four steps:

  1. Fetch the data from the database and save it into an array ($data).
  2. First, create a second array ($formattedData). Then iterate through $data and save its items into $formattedData in such a way, that they can be very easily displayed in chosen HTML structures (div for letter, table for categories).
  3. Iterate through $formattedData and append an item with null as category name for each missing category in the last category row of each letter. Sorry, I wrote here one sentence, but, if you'll read the comments in my code, you'll certainly understand better what I mean.
  4. Display the data in HTML part of the page by iterating through $formattedData and reading its values.

Of course, you can optimize the php code as you wish and/or distribute it into two-three functions. Then you can just call them and assign their returned values to the $data and $formattedData variables.

Note:

If you use my connection code, don't forget to replace my db credentials with yours.


index.php

<?php
require 'Database/dbh.php';

$sql = 'SELECT
            DISTINCT Kategori,
            LEFT(Kategori, 1) AS Letter
        FROM kategorier
        ORDER BY Kategori';

$result = mysqli_query($conn, $sql);

/*
 * Fetch all data at once, into an array like this:
 *
 *  Array
 *  (
 *      [0] => Array
 *          (
 *              [Kategori] => Artiskok
 *              [Letter] => A
 *          )
 *
 *      [1] => Array
 *          (
 *              [Kategori] => Asiatisk
 *              [Letter] => A
 *          )
 *
 *      [2] => Array
 *          (
 *              [Kategori] => Burger
 *              [Letter] => B
 *          )
 *
 *      [...] => [...]
 *
 *  )
 */
$data = mysqli_fetch_all($result, MYSQLI_ASSOC);

/*
 * Free the memory associated with the result. You should
 * always free your result when it is not needed anymore.
 *
 * @link http://php.net/manual/en/mysqli-result.free.php
 */
mysqli_free_result($result);

/*
 * Close the previously opened database connection. Not really needed because
 * the PHP engine closes the connection anyway when the PHP script is finished.
 *
 * @link http://php.net/manual/en/mysqli.close.php
 */
mysqli_close($conn);

/*
 * Iterate through the fetched data and save it into a new array, with a structure suited for the
 * required HTML display. To each letter, a list of category rows is assigned. The new array will
 * look like this, when the maximal number of categories per category row is 2:
 *
 *  Array
 *  (
 *      [A] => Array
 *          (
 *              [0] => Array
 *                  (
 *                      [0] => Aoiuoiiiu
 *                      [1] => Aqewroiuoiiu
 *                  )
 *
 *              [1] => Array
 *                  (
 *                      [0] => Artiskok
 *                      [1] => Asiatisk
 *                  )
 *
 *              [2] => Array
 *                  (
 *                      [0] => Azkajhsdfjkh
 *                  )
 *
 *          )
 *
 *      [B] => Array
 *          (
 *              [0] => Array
 *                  (
 *                      [0] => Bhaskdfhjkh
 *                      [1] => Biuzutt
 *                  )
 *
 *              [1] => Array
 *                  (
 *                      [0] => Burger
 *                  )
 *
 *          )
 *
 *      [...] => [...]
 *
 *  )
 */
$formattedData = [];

// The maximal number of categories per each category row.
$maximalNumberOfCategoriesPerCategoryRow = 2;

// The number of categories per current category row.
$numberOfCategoriesPerCurrentCategoryRow = 0;

// The index of a category row in the list of all category rows assigned to a letter.
$indexOfCurrentCategoryRow = 0;

foreach ($data as $item) {
    $letter = $item['Letter'];
    $category = $item['Kategori'];

    if (!array_key_exists($letter, $formattedData)) {
        /*
         * Assign an item with the current letter as key and an array as value.
         * The array holds all category rows for the current letter.
         */
        $formattedData[$letter] = [];

        // Reset.
        $indexOfCurrentCategoryRow = 0;

        // Reset.
        $numberOfCategoriesPerCurrentCategoryRow = 0;
    }

    // Append the current category to the current category row for the current letter.
    $formattedData[$letter][$indexOfCurrentCategoryRow][] = $category;

    // Increment.
    $numberOfCategoriesPerCurrentCategoryRow++;

    /*
     * If the maximal number of categories per category row is reached...
     *
     * @see "Modulo" operator at https://secure.php.net/manual/en/language.operators.arithmetic.php
     */
    if (
            $numberOfCategoriesPerCurrentCategoryRow %
            $maximalNumberOfCategoriesPerCategoryRow === 0
    ) {
        // Reset.
        $numberOfCategoriesPerCurrentCategoryRow = 0;

        // Increment.
        $indexOfCurrentCategoryRow++;
    }
}

/*
 * Append an item with "null" as category for each missing category in the last
 * category row of each letter. The array holding the formatted data will look
 * like this, when the maximal number of categories per category row is 2:
 *
 *  Array
 *  (
 *      [A] => Array
 *          (
 *              [...] => [...]
 *
 *              [2] => Array
 *                  (
 *                      [0] => Azkajhsdfjkh
 *                      [1] => null
 *                  )
 *
 *          )
 *
 *      [B] => Array
 *          (
 *              [...] => [...]
 *
 *              [1] => Array
 *                  (
 *                      [0] => Burger
 *                      [1] => null
 *                  )
 *
 *          )
 *
 *      [...] => [...]
 *
 *  )
 */
foreach ($formattedData as $letter => $categoryRows) {
    $lastCategoryRow = end($categoryRows);
    $lastCategoryRowKey = key($categoryRows);

    $numberOfCategoriesPerLastCategoryRow = count($lastCategoryRow);

    $numberOfMissingCategoriesInLastCategoryRow = $maximalNumberOfCategoriesPerCategoryRow -
            $numberOfCategoriesPerLastCategoryRow;

    for ($i = 0; $i < $numberOfMissingCategoriesInLastCategoryRow; $i++) {
        // Append an item with "null" as category.
        $formattedData[$letter][$lastCategoryRowKey][] = null;
    }
}

//=====================================================================================
//@todo Just for testing: uncomment the next two lines to display the arrays on screen.
//=====================================================================================
//echo '<pre>' . print_r($data, TRUE) . '</pre>';
//echo '<pre>' . print_r($formattedData, TRUE) . '</pre>';
//=====================================================================================
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes" />
        <meta charset="UTF-8" />
        <!-- The above 3 meta tags must come first in the head -->

        <title>Demo</title>

        <link href="custom.css" type="text/css" rel="stylesheet">
    </head>
    <body>

        <h3>
            Demo: Print a list of categories per category letter, on multiple columns.
        </h3>

        <?php
        if ($formattedData) { /* Data exists */
            foreach ($formattedData as $letter => $categoryRows) {
                ?>
                <div class="categories-container">
                    <div class="letter">
                        <?php echo $letter; ?>
                    </div>
                    <table class="categories">
                        <?php
                        foreach ($categoryRows as $categoryRow) {
                            ?>
                            <tr>
                                <?php
                                foreach ($categoryRow as $category) {
                                    ?>
                                    <td>
                                        <?php echo $category; ?>
                                    </td>
                                    <?php
                                }
                                ?>
                            </tr>
                            <?php
                        }
                        ?>
                    </table>
                </div>
                <?php
            }
        } else { /* No data */
            ?>
            <p class="no-data">
                No data found
            </p>
            <?php
        }
        ?>

    </body>
</html>

custom.css

body {
    margin: 0;
    padding: 20px;
    color: #333;
}

a {
    text-decoration: none;
}

.categories-container {
    margin-bottom: 10px;
}

.letter {
    padding: 10px;
    text-align: left;
    font-weight: 700;
    background-color: #a0c3e5;
}

.categories {
    width: 100%;
    border-spacing: 1px;
    border-collapse: separate;
}

.categories td {
    width: 50%;
    padding: 10px;
    background-color: #f4f4f4;
}

.no-data {
    padding: 10px;
    background-color: #f4f4f4;
}

Database/dbh.php

<?php

/*
 * This page contains the code for creating a mysqli connection instance.
 */

// Db configs.
define('HOST', 'localhost');
define('PORT', 3306);
define('DATABASE', 'tests');
define('USERNAME', 'root');
define('PASSWORD', 'root');

// Error reporting.
error_reporting(E_ALL);
ini_set('display_errors', 1); /* SET IT TO 0 ON A LIVE SERVER! */

/*
 * Enable internal report functions. This enables the exception handling,
 * e.g. mysqli will not throw PHP warnings anymore, but mysqli exceptions
 * (mysqli_sql_exception).
 *
 * MYSQLI_REPORT_ERROR: Report errors from mysqli function calls.
 * MYSQLI_REPORT_STRICT: Throw a mysqli_sql_exception for errors instead of warnings.
 *
 * @link http://php.net/manual/en/class.mysqli-driver.php
 * @link http://php.net/manual/en/mysqli-driver.report-mode.php
 * @link http://php.net/manual/en/mysqli.constants.php
 */
$mysqliDriver = new mysqli_driver();
$mysqliDriver->report_mode = (MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

// Create a new db connection.
$conn = mysqli_connect(HOST, USERNAME, PASSWORD, DATABASE, PORT);

Used data for testing

CREATE TABLE `kategorier` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `Kategori` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `kategorier` (`id`, `Kategori`)
VALUES
    (1,'Artiskok'),
    (2,'Asiatisk'),
    (3,'Burger'),
    (4,'Pizza'),
    (5,'Asiatisk'),
    (6,'Artiskok'),
    (7,'Artiskok'),
    (8,'Durum'),
    (9,'Durum'),
    (10,'Pizza'),
    (11,'Chinaboks'),
    (12,'Azkajhsdfjkh'),
    (13,'Aoiuoiiiu'),
    (14,'Aqewroiuoiiu'),
    (15,'Bhaskdfhjkh'),
    (16,'Biuzutt');

Result

enter image description here


Additional content:

This is an example of how to fetch the data using prepared statements instead of mysqli::query. Note that I only put the data fetching code here. The rest of the code is identical with the omolog part of the above index.php page, which uses mysqli::query.

index.php

<?php

require 'Database/dbh.php';

/*
 * Save the values, with which the database data will be filtered, into variables.
 * These values will replace the parameter markers in the sql statement.
 * They can come, for example, from a POST request of a submitted form.
 */
$letterParam1 = 'A';
$letterParam2 = 'C';
$letterParam3 = 'P';

/*
 * The SQL statement to be prepared. Notice the so-called markers, e.g. the "?" signs. They
 * will be replaced later with the corresponding values when using mysqli_stmt::bind_param.
 *
 * @link http://php.net/manual/en/mysqli.prepare.php
 */
$sql = 'SELECT
            DISTINCT Kategori,
            LEFT(Kategori, 1) AS Letter
        FROM kategorier
        WHERE
            LEFT(Kategori, 1) = ?
            OR LEFT(Kategori, 1) = ?
            OR LEFT(Kategori, 1) = ?
        ORDER BY Kategori';

/*
 * Prepare the SQL statement for execution - ONLY ONCE.
 *
 * @link http://php.net/manual/en/mysqli.prepare.php
 */
$statement = mysqli_prepare($conn, $sql);

/*
 * Bind variables for the parameter markers (?) in the SQL statement that was passed to prepare().
 * The first argument of bind_param() is a string that contains one or more characters which
 * specify the types for the corresponding bind variables.
 *
 * @link http://php.net/manual/en/mysqli-stmt.bind-param.php
 */
mysqli_stmt_bind_param($statement, 'sss'
        , $letterParam1
        , $letterParam2
        , $letterParam3
);

/*
 * Execute the prepared SQL statement. When executed any parameter markers
 * which exist will automatically be replaced with the appropriate data.
 *
 * @link http://php.net/manual/en/mysqli-stmt.execute.php
 */
mysqli_stmt_execute($statement);

/*
 * Get the result set from the prepared statement.
 *
 * NOTA BENE:
 *
 * Available only with mysqlnd ("MySQL Native Driver")! If this is not installed, then
 * uncomment "extension=php_mysqli_mysqlnd.dll" in PHP config file (php.ini) and restart
 * web server (I assume Apache) and mysql service. Or use the following functions instead:
 * mysqli_stmt::store_result + mysqli_stmt::bind_result + mysqli_stmt::fetch.
 *
 * @link http://php.net/manual/en/mysqli-stmt.get-result.php
 * @link https://stackoverflow.com/questions/8321096/call-to-undefined-method-mysqli-stmtget-result
 */
$result = mysqli_stmt_get_result($statement);

/*
 * Fetch all data at once, into an array like this:
 *
 *  Array
 *  (
 *      [0] => Array
 *          (
 *              [Kategori] => Artiskok
 *              [Letter] => A
 *          )
 *
 *      [1] => Array
 *          (
 *              [Kategori] => Asiatisk
 *              [Letter] => A
 *          )
 *
 *      [2] => Array
 *          (
 *              [Kategori] => Burger
 *              [Letter] => B
 *          )
 *
 *      [...] => [...]
 *
 *  )
 */
$data = mysqli_fetch_all($result, MYSQLI_ASSOC);

/*
 * Free the memory associated with the result. You should
 * always free your result when it is not needed anymore.
 *
 * @link http://php.net/manual/en/mysqli-result.free.php
 */
mysqli_free_result($result);

/*
 * Close the prepared statement. It also deallocates the statement handle.
 * If the statement has pending or unread results, it cancels them
 * so that the next query can be executed.
 *
 * @link http://php.net/manual/en/mysqli-stmt.close.php
 */
mysqli_stmt_close($statement);

/*
 * Close the previously opened database connection. Not really needed because
 * the PHP engine closes the connection anyway when the PHP script is finished.
 *
 * @link http://php.net/manual/en/mysqli.close.php
 */
mysqli_close($conn);

/*
 * ---------------------------------------------------------------------------------------------
 * The rest of the page is identical with the omolog part of index.php, which uses mysqli::query
 * ---------------------------------------------------------------------------------------------
 */

// ...
Community
  • 1
  • 1
dakis
  • 225
  • 1
  • 6
  • 32
  • Thanks for the help :D I just still have some questions. When i have 2 thing/data it will make 4 rows/column instead of the 2 that is needed. And can you help me implement the prepared statements? :D – MrSuckAlot Oct 19 '18 at 14:48
  • @MrSuckAlot Hi. You are welcome. Unfortunately, until Wednesday I can't help you with code. I will write you then. But please note that I don't exactly understand what you are asking. So please take your time and explain clearer and in detail until then. – dakis Oct 19 '18 at 20:14
  • I will do my best! – MrSuckAlot Oct 20 '18 at 14:32
  • @MrSuckAlot Hi. I am sorry, but I still can't offer you any code tomorrow, as promised. My computer is broken and, probably, it will take more than a week from now to repair it. – dakis Oct 23 '18 at 17:22
  • Its okay, I found the problem with the code :D The only thing im interested in now, is to secure the code from sql_injections – MrSuckAlot Oct 23 '18 at 21:53
  • @MrSuckAlot Let me give you an advice: Use PDO instead of MySQLi! If you need to learn everything about pdo, mysqli, prepared statements, sql injection, error handling, etc, then read the articles on [this](https://phpdelusions.net) website. It is a very good resource. Don't hesitate to ask anything if you have eventual unclarities. At last, note that also the solutions proposed by _fyrye_ are valid and good. They allow the dynamically changing of the data display by juggling css rules along with php. Good luck. – dakis Oct 24 '18 at 17:11
1

Instead of a Table element, I suggest using a Definition List element dl, since its purpose is your desired display result.

To resolve your issue though, I suggest a few modifications. First, keep in mind that DISTINCT applies to the entire list of columns, not just the column it is next to. Second, ORDER BY from MySQL is typically slower than sorting alphabetically in PHP, and MySQL does not order by alphanumeric values naturally. Lastly, since the data needs to be iterated over alphabetically to determine the first letter to display before moving to the next group, using LEFT() in your MySQL query is not really needed.

Definition List Example: https://3v4l.org/hETha

 $sql = 'SELECT DISTINCT Kategori FROM kategorier;';
 $result = mysqli_query($conn, $sql);
 if ($kategorier = mysqli_fetch_all($result, MYSQLI_ASSOC)) {
    //convert array of arrays to single array - PDO supports PDO::FETCH_GROUP|PDO::FETCH_COLUMN
    $kategorier = array_column($kategorier, 'Kategori');
    //sort the resulting array as a human would
    natsort($kategorier); 
 }
 $previousLetter = null;
 ?>
<dl>
 <?php 
 foreach ($kategorier as $kategori) {
     $currentLetter = strtoupper($kategori[0]);
     if ($previousLetter !== $currentLetter) { ?>
         <dt><h2><?php echo $currentLetter; ?></h2></dt>
     <?php  } ?>
     <dd><?php echo $kategori; ?></dd>
     <?php $previousLetter = $currentLetter; 
     } ?>      
 <dl>

Table Data:

Kategori
---------
Burger
Pizza
Pizza2
Pizza3
Chinaboks
Artiskok
Durum
Asiatisk
Asiatisk2
Asiatisk20
Asiatisk3

Database Results: (MySQL will order by 1, 10, 2, 20, not 1, 2, 10, 20)

[
    ['Kategori' => 'Burger'],
    ['Kategori' => 'Pizza'],
    ['Kategori' => 'Pizza2'],
    ['Kategori' => 'Pizza3'],
    ['Kategori' => 'Chinaboks'],
    ['Kategori' => 'Artiskok'],
    ['Kategori' => 'Durum'],
    ['Kategori' => 'Asiatisk'],
    ['Kategori' => 'Asiatisk2'],
    ['Kategori' => 'Asiatisk20'],
    ['Kategori' => 'Asiatisk3'],
];

Sorted PHP Array Result:

[
    'Artiskok',
    'Artiskok2',
    'Artiskok3',
    'Artiskok20',
    'Asiatisk',
    'Burger',
    'Chinaboks',
    'Durum',
    'Pizza',
    'Pizza2',
    'Pizza3',
];

Resulting HTML Output:

Note: using float will sort from left to right i.e.

1 | 2
3 | 4
5

/* Suggested CSS */
* {
  box-sizing: border-box;
}
dd {
  margin-left: 0;
  float: left;
  width: 50%;
}
dt {
  padding: 12px 0;
  clear: both;
}
<dl>
    <dt><h2>A</h2></dt>
    <dd>Artiskok</dd>
    <dd>Asiatisk</dd>
    <dd>Asiatisk2</dd>
    <dd>Asiatisk3</dd>
    <dd>Asiatisk20</dd>
    <dt><h2>B</h2></dt>
    <dd>Burger</dd>
    <dt><h2>C</h2></dt>
    <dd>Chinaboks</dd>
    <dt><h2>D</h2></dt>
    <dd>Durum</dd>
    <dt><h2>P</h2></dt>
    <dd>Pizza</dd>
    <dd>Pizza2</dd>
    <dd>Pizza3</dd>
<dl>

If you would like to do more complex CSS styling, such as columns sorting. You can use a nested list instead. However it is best to organize the data before iterating over it in the display view.

Nested List Example: https://3v4l.org/bAVGW

//...

$letters = [];
if (!empty($kategorier)) {
    $kategorier = array_column($kategorier, 'Kategori');
    //sort the resulting array as a human would
    natsort($kategorier); 

    foreach ($kategorier as $kategori) {
        $k = strtoupper($kategori[0]);
        if (!isset($letters[$k])) {
             $letters[$k] = [];
        }
        $letters[$k][] = $kategori;
    }
}
?>

<ul class="category-index">
<?php foreach ($letters as $k => $kategorier) { ?>
    <li>
        <h2><?php echo $k; ?></h2>
        <ul class="category">
        <?php foreach ($kategorier as $kategori) { ?>
            <li><?php echo $kategori; ?></li>
        <?php } ?>
        </ul>
    </li>
<?php } ?>
</ul>

Resulting HTML Output:

Note: Using the columns CSS definition, will sort from left-top to bottom-right i.e.

1 | 4
2 | 5
3

/* Suggested CSS */

* {
  box-sizing: border-box;
}

ul.category-index ul,
ul.category-index {
  list-style: none;
  margin: 0;
  padding: 0;
}

ul.category-index li {
  padding: 0;
  margin: 0;
}

ul.category {
  columns: 2;
}
<ul class="category-index">
  <li>
    <h2>A</h2>
    <ul class="category">
      <li>Artiskok</li>
      <li>Asiatisk</li>
      <li>Asiatisk2</li>
      <li>Asiatisk3</li>
      <li>Asiatisk20</li>
    </ul>
  </li>
  <li>
    <h2>B</h2>
    <ul class="category">
      <li>Burger</li>
    </ul>
  </li>
  <li>
    <h2>C</h2>
    <ul class="category">
      <li>Chinaboks</li>
    </ul>
  </li>
  <li>
    <h2>D</h2>
    <ul class="category">
      <li>Durum</li>
    </ul>
  </li>
  <li>
    <h2>P</h2>
    <ul class="category">
      <li>Pizza</li>
      <li>Pizza2</li>
      <li>Pizza3</li>
    </ul>
  </li>
</ul>

As for your desired table element usage. You can use the nested list example PHP code, with the inclusion of the below html. However to ensure the entire table is uniform, you will need to use a single table. colspan="2" can be used for the category index rows, to expand them across the two columns. To determine when a new row is needed, % 2 is used, which will occur on each even iteration of the categories. As well as using count to determine if an empty table cell needs to be added to the current category list and avoid creating an extra empty table row.

Table Example: https://3v4l.org/ZuN20

//...    

$letters = [];
if (!empty($kategorier)) {
    $kategorier = array_column($kategorier, 'Kategori');
    //sort the resulting array as a human would
    natsort($kategorier); 

    foreach ($kategorier as $kategori) {
        $k = strtoupper($kategori[0]);
        if (!isset($letters[$k])) {
             $letters[$k] = [];
        }
        $letters[$k][] = $kategori;
    }
}
?>
<table>
    <tbody>
    <?php foreach($letters as $k => $kategorier) { ?>
        <tr>
           <td colspan="2"><h2><?php echo $k; ?></h2></td>
        </tr>
        <tr>
        <?php 
        $l = count($kategorier);
        if ($l & 1) {
           $kategorier[] = '';
           $l++;
        }
        $y = $l - 1;
        foreach ($kategorier as $i => $kategori) { ?>
            <td><?php echo $kategori; ?></td>
            <?php if ($i % 2 && $i !== $y) { ?></tr><tr><?php } ?>
        <?php } ?>
        </tr>
    <?php } ?>
    </tbody>
</table>

Resulting HTML:

Note: using table will sort from left to right. i.e.

1 | 2
3 | 4
5 |

/* Example CSS */

* {
  box-sizing: border-box;
}

table {
  width: 100%;
}
<table>
  <tr>
    <td colspan="2">
      <h2>A</h2>
    </td>
  </tr>
  <tr>
    <td>Artiskok</td>
    <td>Asiatisk</td>
  </tr>
  <tr>
    <td>Asiatisk2</td>
    <td>Asiatisk3</td>
  </tr>
  <tr>
    <td>Asiatisk20</td>
    <td></td>
  </tr>
  <tr>
    <td colspan="2">
      <h2>B</h2>
    </td>
  </tr>
  <tr>
    <td>Burger</td>
    <td></td>
  </tr>
  <tr>
    <td colspan="2">
      <h2>C</h2>
    </td>
  </tr>
  <tr>
    <td>Chinaboks</td>
    <td></td>
  </tr>
  <tr>
    <td colspan="2">
      <h2>D</h2>
    </td>
  </tr>
  <tr>
    <td>Durum</td>
    <td></td>
  </tr>
  <tr>
    <td colspan="2">
      <h2>P</h2>
    </td>
  </tr>
  <tr>
    <td>Pizza</td>
    <td>Pizza2</td>
  </tr>
  <tr>
    <td>Pizza3</td>
    <td></td>
  </tr>
</table>
Will B.
  • 14,243
  • 4
  • 56
  • 62
  • Hi. First of all, nice examples. I'm interested in your opinion about the following. In the "definition list example" and "table example", in the `foreach` loops of the HTML parts, you are executing some presentation and decision making logic. Let's say, that you are following an MVC approach, where a view class is responsible with the presentation logic and loading+rendering a template file. Regarding each said example, would you put that code in the view class or let it remain in the template file? And why the choice? Thanks. – dakis Oct 06 '18 at 13:23
  • @dakis It depends on many factors, when using strictly `MVC` I separate business and template logic, with the goal to reduce code duplication. Where both the model and template can be reused and functions the same on each use. To me the template that contains HTML is the view. My personal preference is to use as little server side logic as possible to render the view, relying on CSS, Javascript and HTML structures to accomplish the desired display and manipulations of the rendered output. – Will B. Oct 06 '18 at 15:03
  • @dakis In closing most novices are unable to effectively implement separation of concerns without using a framework. While it is best-practice and have often encouraged others to do the same, as it makes programming larger projects easier to troubleshoot and manipulate, it is outside the scope of a procedurally written question and will often only confuse the OP giving them more questions than the answer they were looking for. – Will B. Oct 06 '18 at 15:06
  • I asked you my question just for personal reason; nothing related with the OP question. I was trying to imagine myself, if the kind of presentation logic you used in your answer would be more suitable to apply in my mvc project in certain circumstances. My mvc has a precise workflow and SoC. But I discovered in your answer a certain flexibility, about which I didn't thought before. I am glad that you answered me, because now I have all the pieces to take into consideration. Thank you again. – dakis Oct 06 '18 at 16:52
  • @dakis I would also like to point out that I prefer to use a templating engine rather than raw HTML+PHP. [Twig](https://twig.symfony.com/) for example allows for you to easily extend, embed, include, and override blocks/sections within your templates, that is not very readable in plain PHP. This grants you the flexibility of having base templates that you add on to or reuse. Along with SoC of template logic per template. https://twigfiddle.com/0nl64v Not to mention best-practice enforcement like auto-escaping HTML output from model data. – Will B. Oct 06 '18 at 17:20
  • Thanks for the explanations. Indeed, twig is good. I use it in my project, too. – dakis Oct 07 '18 at 19:48
0

Change to (untested):

while ($row = mysqli_fetch_assoc($result)) {
      if (!isset($lastLetter) || $lastLetter != $row['Letter']) {
            echo "</tr></table>";
            $lastLetter = $row['Letter'];
            echo "<h2>", $row['Letter'], "</h2>";
            echo "<table><tr>";
      }
      echo "<td>" . $row['Kategori'] . "</td>";

}

Hope this helps you out ;)

Cheers

Nicolas

nicolasl
  • 371
  • 2
  • 15
-1

I suggest that you may use Bootstrap Template, and use code as below for the result, the result will separate on two column as you needed.

echo "<div class="col-lg-6 col-md-6 col-sm-6"><table><tr><td>" . $row['Kategori'] . "</td></tr></table></div>";
Sanjiv Dhakal
  • 318
  • 3
  • 13
  • 1
    Your example would create columns of tables. With bootstrap's grid system, the table wouldn't be needed. Additionally he would only need `col-xs-6` to apply `50%` width columns to all viewpoints, otherwise only `col-sm-6` to allow for single column in xs viewpoints and `50%` width in sm or higher viewpoints. – Will B. Oct 06 '18 at 05:18
  • That is on him, how he want the result. But anyway thanks @fyrye for your comment. – Sanjiv Dhakal Oct 06 '18 at 05:23
  • The main issue is that your example does not demonstrate how the OP would be able to display the grouping of letters, along with the individual categories. – Will B. Oct 06 '18 at 05:26