M:N

Time for a new example. My fave podcast is Nobody Listens to Paula Poundstone. A crinkler, cruiser, comedian, and corraler, with experts guests and carousel of house bands, talk about... whatevs.

Let's make a DB of episodes, and people. We can see which people have been on an episode, and which episodes a person has been on. You can download an SQL file that makes tables, and adds data. Import it into your own DB, if you want it on your own machine.

Here are two of the tables. First, the people table.

people table

People table

There are four fields:

  • person_id, the primary key. Required.
  • first_name, e.g., Paula. Required.
  • last_name, e.g., Poundstone. Optional.
  • comments, free text. Optional.
Ethan
Ethan

Wait, why is last_name optional?

Because of people like Cher. Just one name. She won't return Paula's calls, by the way.

Here's the episodes table:

episode shows table

There are four fields describing each episode:

  • episode_id, PK. Required.
  • episode_number, the episode number. Required.
  • title. Required.
  • comment. Optional.
Adela
Adela

Why just use id for the episode number?

That's the first thing you would think of, but it's not a great idea. Suppose the show's producers decided to add a special episode, and called it "XMAS 2021". That can't be an value for the primary key.

Keep ids as numbers that have no meaning in the outside world. They only link rows in the DB, and that's all.

Connecting people and episodes

We want to know which people were on which episodes. This is an M:N, or many-to-many, relationship, since:

  • One person can be on many episodes.
  • One episode can have many people on it.

The rule for representing M:Ns is to make a new table, with the PKs from both sides as foreign keys.

Here's that table:

people_in_episodes table

people_in_episodes table

Person 3 was in episode 1. Person 3 was also in episodes 2, 3, and 4. Episode 2 had person 5 in it. Etc.

What we've done is replace the M:N with two 1:Ns. people has a 1:N relationship with people_in_episodes. That is, one record in people can be related to more than one record in people_in_episodes.

Also, episodes has a 1:N relationship with people_in_episodes. That is, one record in episodes can be related to more than one record in people_in_episodes.

Here's phpMyAdmin's drawing of the DB:

phpMyAdmin's drawing of the DB

A business web app

Now let's write some PHP to get at this data. You can try it. You can also download the code.

Let's see what the app does. Here's the main page:

Main page

There's a navbar, and a links to two reports:

  • A list of episodes
  • A list of people

Here's the list of episodes:

List of shows

The data comes from the database, obviously. Click on a link, and you'll see the episode's deets.

Let's check out some HTML from the show list page. Here are the links to the first three shows:

  • <ul>
  •   <li><a href='episode.php?episode_id=1'>4. The Survivalist!</a></li>
  •   <li><a href='episode.php?episode_id=2'>6. In the House - Music and Mold</a></li>
  •   <li><a href='episode.php?episode_id=5'>81. The Late, Great, Early Man</a></li>

The PHP that makes this page, makes links that include some GET data on the end, like episode.php?episode_id=1.

Click on the first link, and you'll see the deets for a show:

Episode deets

Ethan
Ethan

Hey, the navbar thing changed.

Right. I made the browser window narrower for the last screenshot, to capture all of the comments.

When the browser window is wide, like on a desktop or laptop, the navbar looks like:

Navbar on wide screen

It stretches out.

When the browser window is narrow, like on a phone or tablet, the navbar shrinks to:

Navbar on narrow screen

That happens automatically. Bootstrap handles it.

Adela
Adela

Wow, that is so cool!

Aye, that it is. You'll learn how to do that later. Your sites will look great on any size screen.

Anyway, back to the episode deets screen.

Episode deets

There's the deets for the episode, and a list of the people on the episode. Each person's name is a link to their deets. For example, click on the second link, and see:

Person deets

The person's deets, and a list of the episodes the person has been on.

Try the app. Jump around, and explore the data. Fast, easy to use, with a fun look. What you need in a business app.

The episode list page

Here's the episode list page again:

List of shows

Here's some of the HTML that makes that display.

  • <ul>
  •   <li><a href='episode.php?episode_id=1'>4. The Survivalist!</a></li>
  •   <li><a href='episode.php?episode_id=2'>6. In the House - Music and Mold</a></li>
  •   <li><a href='episode.php?episode_id=5'>81. The Late, Great, Early Man</a></li>

Let's check out the PHP that makes that HTML.

  1. <?php
  2. require_once 'library/useful-stuff.php';
  3. $errorMessage = '';
  4. $sql = "
  5.     SELECT episode_id, episode_number, title
  6.     FROM episodes
  7.     ORDER BY episode_number;";
  8. /** @var PDO $dbConnection */
  9. $stmnt = $dbConnection->prepare($sql);
  10. $stmnt->execute();
  11. // Found any records?
  12. if ($stmnt->rowCount() == 0) {
  13.     $errorMessage = "Sorry, couldn't find episodes in the database.";
  14. }
  15. else {
  16.     $episodeEntities = $stmnt->fetchAll();
  17. }
  18. ?><!doctype html>
  19. ...
  20. <p>Here are the episodes. Click on one for deets.</p>
  21. <?php
  22. if ($errorMessage != '') {
  23.     print "<p class='alert-danger text-danger m-2 p-2'>$errorMessage</p>";
  24. }
  25. else {
  26.     print "<ul>\n";
  27.     foreach ($episodeEntities as $episodeEntity) {
  28.         $id = $episodeEntity['episode_id'];
  29.         $episodeNumber = $episodeEntity['episode_number'];
  30.         $title = $episodeEntity['title'];
  31.         $link = "<a href='episode.php?episode_id=$id'>$episodeNumber. $title</a>";
  32.         print "<li>$link</li>\n";
  33.     }
  34.     print "</ul>\n";
  35. }
  36. ?>
  37. </html>

By line 18, the variable $episodeEntities will be an array of records. Here's part of the array, according to the debugger.

$rows

Each element of $episodeEntities is data for one episode. It's an array, with the fields for that episode.

Let's go further down the PHP. Remember, we want HTML like this:

  • <ul>
  •   <li><a href='episode.php?episode_id=1'>4. The Survivalist!</a></li>
  •   <li><a href='episode.php?episode_id=2'>6. In the House - Music and Mold</a></li>
  •   <li><a href='episode.php?episode_id=5'>81. The Late, Great, Early Man</a></li>

Here's the PHP that makes it:

  • print "<ul>\n";
  • foreach ($episodeEntities as $episodeEntity) {
  •     $id = $episodeEntity['episode_id'];
  •     $episodeNumber = $episodeEntity['episode_number'];
  •     $title = $episodeEntity['title'];
  •     $link = "<a href='episode.php?episode_id=$id'>$episodeNumber. $title</a>";
  •     print "<li>$link</li>\n";
  • }
  • print "</ul>\n";
Ray
Ray

Hey, that's simpler than I thought it would be.

That means you're learning. Show the code to you as you were at the start of the course...

Ray
Ray

Dude, I would have freaked.

True dat.

Showing an episode

Here's one of those links we just saw:

  • <li><a href='episode.php?episode_id=1'>4. The Survivalist!</a></li>

Say the user clicks on it. episode.php will run, with as episode_id of 1.

Here's what they'll see:

Episode

At the top, there's data about the episode: number, title, and comments. Then there's names of people on the episode.

(BTW, Thomas Coyne didn't make it. Do you know where he is? Let me know, if you do.)

Let's work out the data needed to make the page.

We know we want to show episode 1. That's in the URL: episode.php?episode_id=1. Let's start there (1).

Data needed to show episode 1

That will give us the number and name of the episode, and it's description, so we can make the HTML for that:

  • <p>Episode number: 4</p>
  • <p>Title: The Survivalist!</p>
  • <p>Comments: Here's the plan - First, we talk to wilderness survival instructor Thomas Coyne, who will tell Paula what to do the NEXT time she encounters a bear in the woods. Then, estate planner and elder law attorney Sarah Polinsky teaches us how to prepare for the end.</p>

Then we take the episode number, go over to the people_in_episodes table, and get the rows with that episode number (2). That will give us the people ids of 1, 2, and 3.

Then we take the people ids, and find them in the people table (3). Grab those rows, and we'll have what we'll need to make the HTML we need to make links to the each person's page:

  • <h3>People</h3>
  • <ul>
  • <li><a href='person.php?person_id=3'>Adam Felber</a></li>
  • <li><a href='person.php?person_id=1'>Sarah Polinsky</a></li>
  • <li><a href='person.php?person_id=2'>Paula Poundstone</a></li>
  • </ul>

(Sorry for my sloppy drawing. I ain't no artist, as you can see.)

Loading the data

Let's start with the code that grabs the data from the database. Here it is.

  1. <?php
  2. require_once 'library/useful-stuff.php';
  3. $errorMessage = '';
  4. $episodeId = getParamFromGet('episode_id');
  5. $errorMessage = checkEpisodeId($episodeId);
  6. if ($errorMessage == '') {
  7.     // Get episode deets.
  8.     $episodeEntity = getEpisodeWithId($episodeId);
  9.     if (is_null($episodeEntity)) {
  10.         $errorMessage = "Sorry, error looking up episode $episodeId.";
  11.     }
  12. }
  13. if ($errorMessage == '') {
  14.     // Get people in the episode.
  15.     $peopleEntities = getPeopleForEpisodeWithId($episodeId);
  16.     if (is_null($peopleEntities)) {
  17.         $errorMessage = "Sorry, error looking up people in episode with id $episodeId.";
  18.     }
  19. }
  20. ?><!doctype html>

We follow the same pattern as before. Get the episode id from the URL, by calling getParamFromGet(). Then call a validation function. It's much like the others we've written:

  • /**
  •  * Check the episode id.
  •  * @param mixed $id Id.
  •  * @return string Error message, MT if OK.
  •  */
  • function checkEpisodeId($id) {
  •     global $dbConnection;
  •     $errorMessage = '';
  •     if (is_null($id)) {
  •         $errorMessage = 'Sorry, episode id is missing.';
  •     }
  •     if ($errorMessage == '') {
  •         if (! is_numeric($id)) {
  •             $errorMessage = 'Sorry, episode id must be a number.';
  •         }
  •     }
  •     if ($errorMessage == '') {
  •         if ($id < 1) {
  •             $errorMessage = 'Sorry, episode id must be one or more.';
  •         }
  •     }
  •     if ($errorMessage == '') {
  •         // Prepare SQL.
  •         $sql = "
  •             SELECT *
  •             FROM episodes
  •             WHERE episode_id = :id;
  •          ";
  •         $stmt = $dbConnection->prepare($sql);
  •         // Run it.
  •         $isWorked = $stmt->execute(['id' => $id]);
  •         if (!$isWorked) {
  •             $errorMessage = "Sorry, a problem connecting to the database.";
  •         }
  •     }
  •     if ($errorMessage == '') {
  •         // Anything returned?
  •         if ($stmt->rowCount() == 0) {
  •             $errorMessage = "Sorry, no episode with an id of $id.";
  •         }
  •     }
  •     // Return results.
  •     return $errorMessage;
  • }

Again, we're using $errorMessage as a flag. If it's MT, no error. If it's not MT, there is a problem, so skip further processing.

Let's go back to the main program. You'll see:

  • if ($errorMessage == '') {
  •     // Get episode deets.
  •     $episodeEntity = getEpisodeWithId($episodeId);
  •     if (is_null($episodeEntity)) {
  •         $errorMessage = "Sorry, error looking up episode $episodeId.";
  •     }
  • }

I'm extending our code library. Rather than loading the deets of an episode in the page, I'm moving that code to a reusable function. That's common in business web apps. You extract code for common app tasks, like loading data about business entities.

Principle

Reuse

Reuse your code across programs, using functions and page components.

Here's the code for the new function:

  1. /**
  2.  * Get episode entity with a given id.
  3.  * @param int $episodeId The id.
  4.  * @return array|null Entity, null if a problem.
  5.  */
  6. function getEpisodeWithId(int $episodeId) {
  7.     global $dbConnection;
  8.     $result = null;
  9.     // Prepare SQL.
  10.     $sql = "
  11.         SELECT *
  12.         FROM episodes
  13.         WHERE episode_id = :id;
  14.     ";
  15.     $stmt = $dbConnection->prepare($sql);
  16.     // Run it.
  17.     $isWorked = $stmt->execute(['id' => $episodeId]);
  18.     if ($isWorked) {
  19.         $result = $stmt->fetch();
  20.     }
  21.     return $result;
  22. }

Most of it should be familiar, but there are some wrinkles. (I have wrinkles, too. I'm really old. And tired. Soooo tired.)

The function returns the episode record, rather than an error message. Check out the docblock:

  • @return array|null Entity, null if a problem.

It returns the array from $stmt->fetch() (line 19), with the data about the episode.

But it still has to handle errors. If there's a problem, it returns our ol' friend null.

Let's quickly run through it. The function ends up returning a variable called $result (line 21):

  • return $result;

So, the point of the code above it is to set $result. The function starts by making it null (line 8):

  • $result = null;

If everything goes well, that null will be replaced with data about an episode.

Lines 10-17 create and run an SQL query. Here's line 17:

  • $isWorked = $stmt->execute(['id' => $episodeId]);

It has something new there. execute() return a boolean (true/false) value, true if the query worked, false if something went wrong. It doesn't say what went wrong. Maybe Brian from IT spilled vodka into the DB server. Again. He needs an intervention.

Now, the program was already done validation on the id that was passed in, so everything should be OK. So, whatever went wrong, the program can't do anything about it, other than say "something broke."

The next line checks $isWorked, that contains the error flag:

  • if ($isWorked) {
  •     $result = $stmt->fetch();
  • }

Remember that $result has null in it to start. The null gets replaced if everything went OK. The if handles that.

So, we've expanded our code library, with a reusable function that can load an episode, given an id.

W00t! Praise us! PRAISE US!

Back to the main program, and you can see how our functiony friend is called:

  • if ($errorMessage == '') {
  •     // Get episode deets.
  •     $episodeEntity = getEpisodeWithId($episodeId);
  •     if (is_null($episodeEntity)) {
  •         $errorMessage = "Sorry, error looking up episode $episodeId.";
  •     }
  • }

People in an episode

We've got the episode data, but we need to know who was in the episode, so we can make this HTML:

  • <ul>
  • <li><a href='person.php?person_id=3'>Adam Felber</a></li>
  • <li><a href='person.php?person_id=1'>Sarah Polinsky</a></li>
  • <li><a href='person.php?person_id=2'>Paula Poundstone</a></li>
  • </ul>

We lookup the ids of the people in the episode, and the data about the people with those ids. Here's my messy drawing again.

Data needed for the page

Here's the code in the main program that loads the people data:

  • if ($errorMessage == '') {
  •     // Get people in the episode.
  •     $peopleEntities = getPeopleForEpisodeWithId($episodeId);
  •     if (is_null($peopleEntities)) {
  •         $errorMessage = "Sorry, error looking up people in episode with id $episodeId.";
  •     }
  • }
Ethan
Ethan

Hey, you made another function, to do the work.

Right! Another reusable function, in our library. Reuse saves time, and money.

Principle

Reuse

Reuse your code across programs, using functions and page components.

With that money, you could get more dogs, a chocolate fountain, hire Pinkerton to find Thomas Coyne... whatevs.

Georgina
Georgina

Maybe a numismatist got him.

OMG! Groan! (I like it, though.)

Another functiony friend

  1. /**
  2.  * Get the people for the episode with the given id.
  3.  * @param int $episodeId The episode's id.
  4.  * @return array|null The shows.
  5.  */
  6. function getPeopleForEpisodeWithId(int $episodeId) {
  7.     global $dbConnection;
  8.     $result = null;
  9.     $sql = "
  10.         select
  11.             people.person_id as person_id,
  12.             people.first_name as first_name,
  13.             people.last_name as last_name,
  14.             people.comments as comments
  15.         from
  16.             people, people_in_episodes
  17.         where
  18.             people_in_episodes.episode_id = :episode_id_to_show
  19.             and people.person_id = people_in_episodes.person_id
  20.         order by last_name;";
  21.     /** @var PDO $dbConnection */
  22.     $stmnt = $dbConnection->prepare($sql);
  23.     $isWorked = $stmnt->execute([':episode_id_to_show' => $episodeId]);
  24.     if ($isWorked) {
  25.         $result = $stmnt->fetchAll();
  26.     }
  27.     return $result;
  28. }

Much the same as the last function, with two differences. First, the query is more complex.

  • Start with what we know: the episode id.
  • Find rows in people_in_episodes with that id: people_in_episodes.episode_id = :episode_id_to_show
  • Track that over to people: and people.person_id = people_in_episodes.person_id

Remember: some people don't like this key chain method, and will want to use JOIN.

The second difference is more subtle. Check out line 25:

  • $result = $stmnt->fetchAll();

The last function used fetch(), since it was grabbing only one row. This functiony friend returns a bunch of records, so it uses fetchAll().

Making the HTML

After this is done...

  • $episodeEntity = getEpisodeWithId($episodeId);
  • ...
  • $peopleEntities = getPeopleForEpisodeWithId($episodeId);

... we have the data needed for the page. $episodeEntity has the DB row about the episode. Title, description, like that. $peopleEntities is an array of DB rows. Each one is a row for one person.

Now we make the HTML, using this data. Here's the code for making the HTML.

  1. <?php require_once 'library/regions/top.php'; ?>
  2. <h1>Episode</h1>
  3. <?php
  4. if ($errorMessage != '') {
  5.     print "<p class='alert-danger text-danger m-2 p-2'>$errorMessage</p>";
  6. }
  7. else {
  8.     print "<p>Here are the deets for the episode.</p>\n";
  9.     print "<p>Episode number: {$episodeEntity['episode_number']}</p>\n";
  10.     print "<p>Title: {$episodeEntity['title']}</p>\n";
  11.     print "<p>Comments: {$episodeEntity['comments']}</p>\n";
  12.     if (count($peopleEntities) == 0) {
  13.         print "<p>No people are listed for the show.</p>\n";
  14.     }
  15.     else {
  16.         print "<h3>People</h3>\n";
  17.         print "<ul>\n";
  18.         foreach ($peopleEntities as $personEntity) {
  19.             $personId = $personEntity['person_id'];
  20.             $firstName = $personEntity['first_name'];
  21.             $lastName = $personEntity['last_name'];
  22.             $link = "<a href='person.php?person_id=$personId'>$firstName $lastName</a>";
  23.             print "<li>$link</li>\n";
  24.         }
  25.         print "</ul>\n";
  26.     }
  27. }
  28. ?>

As usual, test for errors first (line 4). If there aren't any, then show the basic data for the episode (lines 9-11). If there's people data, then:

  • foreach ($peopleEntities as $personEntity) {

Run through the $peopleEntities array, one element at a time. For each one, make HTML to show a link to a page for that person:

  • $link = "<a href='person.php?person_id=$personId'>$firstName $lastName</a>";

Up next

In the last required part of the course, we'll see how to make forms, to help users enter data into a DB.