1

I'm having big trouble trying to create a while loop within a while loop. The first while loop is supposed to output categories. The second while loop is supposed to output the links associated with the category. The link information is in a links table and the categories information in the category table. I'm trying to do a JOIN on links.catID on category.ID.

Links table:
id links url catID type

Category table:
id cat type

links.catID is supposed to equal category.id in ints. category.cat would then output the name of the category.

The code I have is:

require 'db/connect.php';
$sql = "SELECT * FROM category";
$results = $db->query($sql);
echo "<p class='post-footer align-left'><span class='pageName'><span 
class='style3'>Links</span></span></p>";
    if($results->num_rows){
        while($row = $results->fetch_object()){
        echo "<table width='507' border='1'>";
        echo " <p class='post-footer align-left'><span class='pageName'><span class='style3'>{$row->cat}</span></span></p>";
        $sql1 = "SELECT links.url, links.links, links.catID FROM links INNER JOIN category on category.id WHERE category.id = links.catID";
        $results1 = $db->query($sql1);
            if ($results1->num_rows > 0) {
                while($row1 = $results1->fetch_object()){
                    echo "<th><span class='pageName'><span class='style3'>{$row1->links}</span></span></th>";
                }
            }
        }
    }

Please be kind to me, I have very little experience and that very likely shows in the above. I have been stuck on this for a while now and just can't figure it out. I would really appreciate someone's help.

I think I have it explained properly above :)

Don't worry about the CSS of the PHP one. The CSS is easy to fix. Just need to only list the right links with the right categories.

enter image description here

enter image description here

RiggsFolly
  • 93,638
  • 21
  • 103
  • 149
Conal Smith
  • 31
  • 1
  • 6
  • 1
    Your explanations are properly. But it would be good to give us a view of what you expect to have in the end. For example the resulting html code (after rendering), or maybe an image. – PajuranCodes Jul 21 '18 at 20:19
  • Ya that populates the same information that I had in my SELECT statement. The thing is that it populates all the links in the database under each of the categories. Therefor I believe it's an issue in the if statement but I've tried more or less everything I can think of over the last week or so and no good :( – Conal Smith Jul 21 '18 at 20:21
  • @ConalSmith, I still cannot understand what exactly is wrong here. If something is wrong with displaying of data, could you please make a screen shot and mark what should look differently and how should it look? – Danielius Jul 21 '18 at 20:26
  • I've never used this before. How do I input an image? I have a temp one hard coded with HTML, but I want to use PHP so others can easily edit it. Basically what I'm looking for is the integer in links.catID to equal category.ID. Under each category this is true it will list the links. – Conal Smith Jul 21 '18 at 20:26
  • @ConalSmith, to add an image, edit your post and choose the icon at right of the one that you used for code formatting and it will trigger "drag and drop images here" screen. – Danielius Jul 21 '18 at 20:28
  • I suggest to just write for us the html code which should result after the php code fills the values. So, the final html file, so to say. Then we can find a solution. For us is not necessarily important what you, personaly, want to try, but what the result should look like, so that we can suggest our solution to you. – PajuranCodes Jul 21 '18 at 20:35
  • Is each row in `links` table associated to a `catID`? – PajuranCodes Jul 21 '18 at 20:47
  • Is your code part of a function, or it's just a part of a php web page? – PajuranCodes Jul 21 '18 at 20:52
  • catID is part of the links table. The foreign key is id in the category table. The cat in the category is the name of the category. No this is not part of a function. To be honest I'm not advanced enough to use functions as I don't yet understand how they work. I'm trying to teach myself at the moment. So it's part of a php page only. – Conal Smith Jul 21 '18 at 21:37
  • I knew all these. I was just asking if you actually enforce the existence of a `catID` value in each `links` row. This was important, so that I know if I should apply `LEFT JOIN` or an `INNER JOIN`. But iit doesn't matter for the moment so much. – PajuranCodes Jul 21 '18 at 21:57
  • I have tried using both INNER and LEFT JOINs but struggling here really lol. Any help would be greatly appreciated. – Conal Smith Jul 21 '18 at 22:03
  • Sorry miss read your question. Yes I absolutely enforce the existence of a catID. If the user doesn't add one it will default to id = 1 which is equal to the category General Link. – Conal Smith Jul 21 '18 at 22:25

3 Answers3

1

As a follow-up to my first answer, the example below shows how you can do this with a single query. IMHO, you should always opt for this approach. If you only have a handful of categories to drill into, it may not be much of a performance drain if you stick with the query-nested-inside-a-loop approach. But if you have a sizable number of categories, the approach below should be significantly more efficient:

require 'db/connect.php';
$sql = "
    SELECT
        category.id
        , links.links
    FROM
        category
    LEFT JOIN
        links ON links.catID = category.id
    ORDER BY
        category.id ASC
        -- this ORDER BY is critical, it ensures that all of the
        -- records will be lumped by category
";
// (yes, this query will create multiple rows for every category, but that's OK,
// we're going to deal with that in the while() loop
$results = $db->query($sql);
echo "<p class='post-footer align-left'><span class='pageName'><span class='style3'>Links</span></span></p>";
if($results->num_rows){
    $previousId = '';
    while($row = $results->fetch_object()){
        $currentId = $row->id;
        if ($currentId !== $previousId) {
            // we only show the wrapper <table>, and the category header, if we've encountered a new
            // batch of rows which signifies that a new category is being processed
            echo "<table width='507' border='1'>";
            echo " <p class='post-footer align-left'><span class='pageName'><span class='style3'>{$row->cat}</span></span></p>";
        }
        echo "<th><span class='pageName'><span class='style3'>{$row->links}</span></span></th>";
        if ($currentId !== $previousId and $previousId !== '') {
            echo "</table>";  // this wasn't in your original code, but I assume it should be here
        }
        $previousId = $currentId;
    }
}
echo "</table>";
1

Some principles to follow:

  • Avoid writing php code which contains html tags in it.
  • Don't mix the php code which fetches db data with the html code. In my code you'll notice that I fetched and processed the db data only on the upper side of the web page. So completely separated from the html code. In the html code I am using only the arrays. Why is this important? Because, for example, if a web designer should customize the design of your pages, he should be able to change the html codes without beeing obliged (by your code) to learn any database access commands. Another reason is, that, at some point, you will probably want to separate the data access code from the html code. You can only do this without mixing them. So, in short, the html code should not do any processing, but only reading operations (from an array, from variables).
  • In general, avoid using while, or do while constructs. They introduce a big risc of infinite loops, which can break your apps, or systems. In case of the while loops used in mysqli, it would be ok to use them, but with caution. Make sure that no infinite loops can be activated.
  • Consider each request made by your codes to a database as an expensive operation. For example, if you are fetching a list with 500 categories, and, for each category, you are making a new request to database to fetch the corresponding links list, then you'll "overwhelm" your web server and database.
  • Consider applying prepared statements instead of directly querying the database with query. Make yourself a habit from always using them. They are very important, because with them you are avoiding the so-called mysql injections.
  • Consider naming the tables with plurals. Like "categories". And don't hesitate to give good understandable database/table/column names. Use the form "this_is_the_name" for them (with separating underscore and small characters). In php use the "camelCase" syntax for the php variable/function/method names. Like $mysqlConnection. The id's of the html controls should be camelCase too: "thisIsAnInputId". And the css classes like this: "this-is-a-div-class".
  • Write echo '<table width="507" border="1">';. It looks better, isn't it? I am using the single quotes almost everywhere, instead of double quotes. In rare occasions I must escape the single quote character with a backslash, like this: echo 'I don\'t know';

Some good resources:


index.php:

<?php
require 'db/connect.php';

/*
 * The SQL statement. The external values are passed directly into it.
 * Therefore, the code is open to SQL injections. In order to avoid
 * them, an sql statement receiving external values should always be
 * prepared for execution.
 *
 * @link http://php.net/manual/en/mysqli.prepare.php
 */
$sql = 'SELECT
            c.id AS categoryId,
            c.cat,
            c.type,
            l.id AS linkId,
            l.url,
            l.links
        FROM links AS l
        INNER JOIN category AS c ON l.catID = c.id';

/*
 * Performs a query on the database.
 *
 * Returns FALSE on failure [<< and raises warnings, fatal errors, exceptions, etc >>].
 * For successful SELECT, SHOW, DESCRIBE or EXPLAIN queries mysqli_query() will return a
 * mysqli_result object. For other successful queries will return TRUE.
 *
 * @link https://secure.php.net/manual/en/mysqli.query.php
 */
$result = $db->query($sql);

/*
 * Fetch the data into an array like this:
 *
 * Array
 * (
 *     [0] => Array
 *         (
 *             [categoryId] => 1
 *             [cat] => Platforms
 *             [type] =>
 *             [linkId] => 1
 *             [url] => #
 *             [links] => CSI (Internet Explorer)
 *         )
 *
 *     [1] => Array
 *         (
 *             [categoryId] => 1
 *             [cat] => Platforms
 *             [type] =>
 *             [linkId] => 2
 *             [url] => #
 *             [links] => Customer view of Service Cloud
 *         )
 *
 *     [...] => [...]
 *
 *     [5] => Array
 *         (
 *             [categoryId] => 2
 *             [cat] => Engineering
 *             [type] =>
 *             [linkId] => 6
 *             [url] => #
 *             [links] => Check Returned OPTs
 *         )
 *
 *     [6] => Array
 *         (
 *             [categoryId] => 2
 *             [cat] => Engineering
 *             [type] =>
 *             [linkId] => 7
 *             [url] => #
 *             [links] => Eng Links
 *         )
 *
 *     [...] => [...]
 *
 * )
 */
$data = $result->fetch_all(MYSQLI_ASSOC);

/*
 * Iterate through the fetched data and save it into a new array, with
 * a structure suited for the required display. The new array looks like this:
 *
 * Array
 * (
 *     [1] => Array
 *         (
 *             [categoryName] => Platforms
 *             [categoryType] => red
 *             [links] => Array
 *                 (
 *                     [0] => Array
 *                         (
 *                             [1] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => CSI (Internet Explorer)
 *                                 )
 *
 *                             [2] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => CSI (Backup link - Don't have link)
 *                                 )
 *
 *                         )
 *
 *                     [1] => Array
 *                         (
 *                             [3] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Customer view of Service Cloud
 *                                 )
 *
 *                             [4] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Service Cloud
 *                                 )
 *
 *                         )
 *
 *                     [2] => Array
 *                         (
 *                             [5] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Open to Suggestions
 *                                 )
 *
 *                         )
 *
 *                 )
 *
 *         )
 *
 *     [2] => Array
 *         (
 *             [categoryName] => Engineering
 *             [categoryType] => blue
 *             [links] => Array
 *                 (
 *                     [0] => Array
 *                         (
 *                             [6] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Check Returned OPTs
 *                                 )
 *
 *                             [7] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => EMC Caller
 *                                 )
 *
 *                         )
 *
 *                     [1] => Array
 *                         (
 *                             [8] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Eng Links
 *                                 )
 *
 *                             [9] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Eng RCA Database
 *                                 )
 *
 *                         )
 *
 *                     [2] => Array
 *                         (
 *                             [10] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => OPT
 *                                 )
 *
 *                             [11] => Array
 *                                 (
 *                                     [linkUrl] => #
 *                                     [linkLinks] => Surge (Internet Explorer)
 *                                 )
 *
 *                         )
 *
 *                 )
 *
 *         )
 *
 * )
 */
$formattedData = [];

// The number of items (maximal 2) per each "links row" item.
$numberOfItemsPerLinksRow = 0;

// The index of a "links row" item.
$indexOfCurrentLinksRow = 0;

foreach ($data as $item) {
    // Extract category data.
    $categoryId = $item['categoryId'];
    $categoryName = $item['cat'];
    $categoryType = $item['type'];

    // Add a category item if not already exist, with the category id as the item's key.
    if (!array_key_exists($categoryId, $formattedData)) {
        $formattedData[$categoryId] = [];
        $formattedData[$categoryId]['categoryName'] = $categoryName;
        $formattedData[$categoryId]['categoryType'] = $categoryType;

        // In this category item add an array to hold the list of links items.
        $formattedData[$categoryId]['links'] = [];

        // Reset.
        $indexOfCurrentLinksRow = 0;

        // Reset.
        $numberOfItemsPerLinksRow = 0;
    }

    // Extract link data.
    $linkId = $item['linkId'];
    $linkUrl = $item['url'];
    $linkLinks = $item['links'];

    // Add the details of the link as an item in the "links row" item. Notice the link id as key.
    $formattedData[$categoryId]['links'][$indexOfCurrentLinksRow][$linkId] = [
        'linkUrl' => $linkUrl,
        'linkLinks' => $linkLinks,
    ];

    // Increment.
    $numberOfItemsPerLinksRow++;

    if ($numberOfItemsPerLinksRow % 2 === 0) {
        // Reset.
        $numberOfItemsPerLinksRow = 0;

        // Increment.
        $indexOfCurrentLinksRow++;
    }
}
?>
<!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>

        <style type="text/css">
            body {
                margin: 0;
                padding: 20px;
                color: #333;
            }

            a {
                text-decoration: none;
            }

            .category {
                padding: 10px;
                margin-bottom: 10px;
                text-align: left;
                font-weight: 700;
                border: 1px solid #ccccff;
                background-color: #f3f8fd;
            }

            .links {
                width: 100%;
                border-spacing: 1px;
                border: 1px solid #fafafa;
            }

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

            .links td a {
                color: #0033ff;
                font-weight: 700;
                background-color: #f8f8f8;
            }

            .no-data {
                padding: 10px;
                margin-bottom: 10px;
                border: 1px solid #ddd;
                background-color: #f4f4f4;
            }
        </style>
    </head>
    <body>

        <h3>
            Categories
        </h3>

        <?php
        if ($formattedData) { // Data exists.
            foreach ($formattedData as $categoryId => $categoryItem) {
                ?>
                <p class="category">
                    <?php echo $categoryItem['categoryName']; ?>
                </p>

                <table class="links">
                    <?php
                    $links = $categoryItem['links'];

                    foreach ($links as $linksRow) {
                        ?>
                        <tr>
                            <?php
                            foreach ($linksRow as $linkId => $linkItem) {
                                ?>
                                <td>
                                    <a href="<?php echo $linkItem['linkUrl']; ?>">
                                        <?php echo $linkItem['linkLinks']; ?>
                                    </a>
                                </td>
                                <?php
                            }

                            // If there is no second link item in this "links row" item, then create an empty cell.
                            $numberOfItemsPerLinksRow = count($linksRow);
                            if ($numberOfItemsPerLinksRow === 1) {
                                ?>
                                <td>
                                    &nbsp;
                                </td>
                                <?php
                            }
                            ?>
                        </tr>
                        <?php
                    }
                    ?>
                </table>
                <?php
            }
        } else { // No data.
            ?>
            <p class="no-data">
                No data.
            </p>
            <?php
        }
        ?>

    </body>
</html>

db/connect.php:

Don't forget to change the database credentials.

<?php

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

/*
 * 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.
 *
 * @see http://php.net/manual/en/mysqli.construct.php
 */
$db = new mysqli(HOST, USERNAME, PASSWORD, DATABASE, PORT);

Used definition and data for table "category":

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

INSERT INTO `category` (`id`, `cat`, `type`)
VALUES
    (1, 'Platforms', 'red'),
    (2, 'Engineering', 'blue');

Used definition and data for table "links":

CREATE TABLE `links` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `links` varchar(100) DEFAULT NULL,
  `url` varchar(100) DEFAULT NULL,
  `catID` int(11) unsigned DEFAULT NULL,
  `type` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `catID` (`catID`),
  CONSTRAINT `links_ibfk_1` FOREIGN KEY (`catID`) REFERENCES `category` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

INSERT INTO `links` (`id`, `links`, `url`, `catID`, `type`)
VALUES
    (1, 'CSI (Internet Explorer)', '#', 1, NULL),
    (2, 'CSI (Backup link - Don\'t have link)', '#', 1, NULL),
    (3, 'Customer view of Service Cloud', '#', 1, NULL),
    (4, 'Service Cloud', '#', 1, NULL),
    (5, 'Open to Suggestions', '#', 1, NULL),
    (6, 'Check Returned OPTs', '#', 2, NULL),
    (7, 'EMC Caller', '#', 2, NULL),
    (8, 'Eng Links', '#', 2, NULL),
    (9, 'Eng RCA Database', '#', 2, NULL),
    (10, 'OPT', '#', 2, NULL),
    (11, 'Surge (Internet Explorer)', '#', 2, NULL);

Output:

Output image

PajuranCodes
  • 303
  • 3
  • 12
  • 43
  • really appreciate your help and extremely detailed explanations and best practices. Really helped, I will definitely loop to keeping the PhP and the HTML code separate in future, what you said made complete sense. This has worked for me, as did Adams answer. – Conal Smith Jul 22 '18 at 06:49
  • @ConalSmith I am glad to have helped a bit and that you found the solution. Good luck. P.S: This php-html separation has the name [Separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) and is the principle on which the MVC pattern-based applications are built. – PajuranCodes Jul 22 '18 at 08:24
0

The comments under the original question indicate:

The thing is that it populates all the links in the database under each of the categories.

Your problem comes from the fact that your nested query is a static string.

In this query:

$sql1 = "SELECT links.url, links.links, links.catID FROM links INNER JOIN category on category.id WHERE category.id = links.catID";

There is no variable. So no matter which category you're currently evaluating in the while loop, it looks like you're probably getting back all the links for all the categories. I'm assuming that you wanted to do something like this?

$sql1 = "SELECT links.url, links.links, links.catID FROM links INNER JOIN category on category.id WHERE category.id = links.catID AND category.id = {$row->id}";

Also, please keep in mind that your overall approach is inefficient. Almost any time you're calling a query for every iteration of a while loop, it's inefficient. Ideally, you would pull back all of this data in the first query, using a proper set of JOINs, and then filter through them in the while loop.

  • 1
    Hey Adam, Really appreciate this, it's so obvious where I was going wrong now lol. Thanks for pointing that out to me. And also thanks for providing the answer on how to implement it in one query as well. Really appreciate it :) – Conal Smith Jul 22 '18 at 06:51