Making entities

Emily's boat pups
Emily's boat pups

In the last lesson, we had two pages.

  • Show a form, validate user data, if it's OK, send it for processing.
  • A processing page.

We can expand on that, to make a workflow for making business entities. A business entity is something the business needs to store data about. Customers, products, transactions, camels, ships, comedians... whatever is relevant to the business.

Usually there's a DB table for storing business entity data. Often there's more than one. More on that later.

OK, suppose "comedian" is a business entity. We want to store the following data for comedians:

  • Name, e.g., Aisling Bea
  • Comment, e.g., One of the best!

Let's make some pages that let someone make a new comedian.

DB table

Here's data in the comedians table, in a DB:

Comedians

Three fields:

Fields

We've got a primary key, a 50-character string field for name, and a text field for comments. In MySQL, a text field can hold up to 65,535 characters (64K - 1, if you care).

Now, one thing an app will do is help users make new business entities. Let's see how that works.

What users see

First, we need to work out what we want users to see. Programming is like that. You work out the goal, then you know what to make.

In fact, the most common reason that systems fail has nothing to do with tech. It's that nobody understood what the system should actually do to meet business requirements. Never be in a hurry to make something, if you don't know what it should do.

Principle

Get requirements right

Make sure you know what a program is supposed to do, before you write it.

So, what should the app do, to help users create new comedians? How about this:

Entity creation workflow

The user first sees a form to fill in, with a Save button (1).

We know how to do that.

Form page

They type in their data, and click save (2). That POSTs the data back to the page, which reloads itself.

Here's the code.

  1. <?php
  2. /**
  3.  * Create/edit comedian.
  4.  * This page validates, as well as showing the form.
  5.  */
  6. session_start();
  7. require_once 'library/useful-functions.php';
  8. $errorMessage = '';
  9. // Set default values for fields.
  10. $comedianName = '';
  11. $comedianComments = null;
  12. // Is there post data?
  13. if ($_POST) {
  14.     // User filled in data and sent it to this page.
  15.     // Grab the data sent.
  16.     $comedianName = getParamFromPost('name');
  17.     $comedianComments = getParamFromPost('comments');
  18.     // Validate.
  19.     $errorMessage .= checkName($comedianName);
  20.     // Is everything OK?
  21.     if ($errorMessage == '') {
  22.         // No errors.
  23.         // Stash data for processing later.
  24.         $_SESSION['comedian_name'] = $comedianName;
  25.         $_SESSION['comedian_comments'] = $comedianComments;
  26.         // Off to processing.
  27.         header('Location: save-new-comedian.php');
  28.         exit();
  29.     }
  30. }
  31. ?><!doctype html>
  32. <html lang="en">
  33.     <head>
  34.         <?php
  35.         $pageTitle = 'Add comedian';
  36.         require_once 'library/page-components/head.php';
  37.         ?>
  38.     </head>
  39.     <body>
  40.         <?php
  41.         require_once 'library/page-components/top.php';
  42.         print "<h1>$pageTitle</h1>\n";
  43.         if ($errorMessage != '') {
  44.             print "<p class='error-message'>$errorMessage</p>\n";
  45.         }
  46.         ?>
  47.         <form method="post">
  48.             <p>
  49.                 <label>Name:
  50.                   <input type="text" name="name" value="<?php
  51.                   if (! is_null($comedianName)) {
  52.                       print $comedianName;
  53.                   }
  54.                   ?>">
  55.  
  56.                     <input type="text" name="name" value="<?php print $comedianName; ?>">
  57.                 </label>
  58.             </p>
  59.             <p>
  60.                 <label>Comments:
  61.                     <textarea name="comments"><?php
  62.                         if (! is_null($comedianComments)) {
  63.                             print $comedianComments;
  64.                         }
  65.                     ?></textarea>
  66.                 </label>
  67.             </p>
  68.             <p>
  69.                 <button type="submit">Save</button>
  70.             </p>
  71.         </form>
  72.         <?php
  73.         require_once 'library/page-components/footer.php';
  74.         ?>
  75.     </body>
  76. </html>

Line 13 checks for POST data. If it's there, this page is running after the user typed in some data, and pressed Save. You can see that the form tag doesn't have an action (<form method="post">), so the page sends its data to itself.

Line 16 gets the name from the POST data:

  • $comedianName = getParamFromPost('name');

Getting a value from POST data is a common task, so I made a function for it, as we've seen. It's in the library useful-functions.php:

  • /**
  •  * Get a field value from POST.
  •  * @param string $fieldName Name of the field.
  •  * @return string|null Trimmed value, or null if field not found or is MT.
  •  */
  • function getParamFromPost(string $fieldName) {
  •     $value = null;
  •     if (isset($_POST[$fieldName])) {
  •         $value = $_POST[$fieldName];
  •         if (is_string($value)) {
  •             $value = trim($value);
  •         }
  •         if ($value == '') {
  •             $value = null;
  •         }
  •     }
  •     return $value;
  • }

The function standardizes the value returned when a widget has no data. It returns null.

Remember, null is a special value that means unknown, or nothing.

Ethan
Ethan

What's the difference between an MT string, '', and null?

null is a value you can store in a numeric database field, to show the data is missing. It has a special meaning in DB land. You can't store an MT string into a numeric field in a database.

There's a validation function for the name field. Call the function like this:

  • $errorMessage .= checkName($comedianName);

It returns an error message, or MT if there are no errors.

Adela
Adela

Why is there a . before the =? The . is append, right, like & in VBA?

Yes, the . is append. If you have more than one field that needs validation, you want to show all of the error messages at the same time. So, you want to accumulate the error messages.

Here's the validation function. The only rule is the name is required.

  • /**
  •  * @param string $userInput User input to check.
  •  * @return string Error message, MT if data is OK.
  •  */
  • function checkName($userInput) {
  •     $errorMessage = '';
  •     if (is_null($userInput)) {
  •         $errorMessage = 'Sorry, you must enter a name.<br>';
  •     }
  •     return $errorMessage;
  • }

Notice how you call this:

  • $comedianName = getParamFromPost('name');
  • ...
  • $errorMessage .= checkName($comedianName);

If the user doesn't enter a name, then getParamFromPost() will return null. That's why checkName() tests for null.

If the data is OK, it's stashed into the session, and the browser is told to go to process-simple-form-data.php.

A new widget: textarea

Thar's a new widget in town, pardner.

  • <label>Comments:
  •     <textarea name="comments"><?php
  •         if (! is_null($comedianComments)) {
  •             print $comedianComments;
  •         }
  •     ?></textarea>
  • </label>

A textarea is a widget for multiline text input. This HTML...

  • <label>Joke<br>
  •     <textarea name="joke" rows="5" cols="40">
  • A software tester goes into a bar. They order 1 beer, 0 beers, 999999999 beers, -1 beers, a lizard, and aasdfgjk.
  •     </textarea>
  • </label>

... shows as:

Try editing the text in the box.

textarea has a name attribute, just like input. The rows and cols attributes control...er, the number of rows and columns.

The text in the widget is between the open and close textarea bits of the tag. Here's our code again:

  • <label>Comments:
  •     <textarea name="comments"><?php
  •         if (! is_null($comedianComments)) {
  •             print $comedianComments;
  •         }
  •     ?></textarea>
  • </label>

If $comedianComments is not null, it's placed into the textarea.

Processing

Here's what we want processing to do.

Processing

Here's code for the processing page:

  1. <?php
  2. session_start();
  3. // Connect to the database.
  4. require_once 'library/db-connect.php';
  5. // Get the data to save.
  6. $name = $_SESSION['comedian_name'];
  7. $comments = $_SESSION['comedian_comments'];
  8. // Save the data into the DB.
  9. $sql = "
  10.     INSERT INTO comedians (
  11.         name, comments
  12.     )
  13.     VALUES (
  14.         :name, :comments
  15.     )
  16. ";
  17. /** @var PDO $dbConnection */
  18. $stmnt = $dbConnection->prepare($sql);
  19. $stmnt->execute([
  20.     'name' => $name,
  21.     'comments' => $comments
  22. ]);
  23. // Get the id of the new comedian.
  24. $comedianId = $dbConnection->lastInsertId();
  25. // Jump to the view page, passing new record id.
  26. header("Location: view-comedian.php?comedian_id=$comedianId");
  • Get the data from the session (lines 2, 6, and 7).
  • Make an SQL INSERT statement (lines 9-16)
  • Execute the statement, with the user's data (lines 18-22).
  • Get the id of the new entity (line 24).
  • Jump to the viewing page, passing the id (line 26).

More about line 24. Here's the table definition again:

Fields

Each record has three fields: comedian_id, name, and comments. But the SQL...

  • INSERT INTO comedians (
  •     name, comments
  • )
  • VALUES (
  •     :name, :comments
  • )

...gives values for the name and comments field, but not for the comedian_id.

Reflect

Why doesn't the SQL INSERT give a value for comedian_id

If you were logged in as a student, the lesson would pause here, and you'd be asked to type in a response. If you want to try that out, ask for an account on this site.
Georgina
Georgina

Oo! Because it's an auto-increment field. MySQL chooses the value itself.

Correct!

But that leaves us with a problem. We want to send the id of the new record to the view page...

  • header("Location: view-comedian.php?comedian_id=$comedianId");

... so it can show the new record. But because MySQL chose the value, we don't know what it is.

The solution is to ask MySQL, like this:

  • $stmnt = $dbConnection->prepare($sql);
  • $stmnt->execute([
  •     'name' => $name,
  •     'comments' => $comments
  • ]);
  • // Get the id of the new comedian.
  • $comedianId = $dbConnection->lastInsertId();
  • // Jump to the view page, passing new record id.
  • header("Location: view-comedian.php?comedian_id=$comedianId");

Showing the new entity

Here's the workflow again:

Workflow

After making the new comedian entity, we want to show it. Like this:

New comedian

The id is passed at the end of the URL. Here's the code.

  1. <?php
  2. require_once 'library/useful-stuff.php';
  3. $errorMessage = '';
  4. // Get id from URL.
  5. $comedianId = getParamFromGet('comedian_id');
  6. $errorMessage = checkComedianId($comedianId);
  7. if ($errorMessage == '') {
  8.     $comedianEntity = getComedianWithId($comedianId);
  9.     if (is_null($comedianEntity)) {
  10.         $errorMessage = "Sorry, problem loading comedian with id $comedianId.";
  11.     }
  12.     else {
  13.         // Put the field values into variables.
  14.         $comedianName = $comedianEntity['name'];
  15.         $comedianComments = $comedianEntity['comments'];
  16.     }
  17. }
  18. ?><!doctype html>
  19. <html lang="en">
  20.     <head>
  21.         <?php
  22.         $pageTitle = 'Comedian';
  23.         require_once 'library/page-components/head.php';
  24.         ?>
  25.     </head>
  26.     <body>
  27.         <div class="container">
  28.             <?php
  29.             require_once 'library/page-components/top.php'
  30.             ?>
  31.             <h1>Comedian</h1>
  32.             <?php
  33.             if ($errorMessage != '') {
  34.                 print "<p class='error-message'>$errorMessage</p>";
  35.             }
  36.             else {
  37.                 print "<p>Name: $comedianName</p>\n";
  38.                 print "<p>Comments: $comedianComments</p>\n";
  39.             }
  40.             require_once 'library/page-components/footer.php';
  41.             ?>
  42.     </body>
  43. </html>

Nothing new here. Notice that I've new functiony friends. One is:

  • /**
  •  * Get comedian entity with a given id.
  •  * @param int $comedianId The id.
  •  * @return array|null Entity, null if a problem.
  •  */
  • function getComedianWithId(int $comedianId) {
  •     global $dbConnection;
  •     $result = null;
  •     // Prepare SQL.
  •     $sql = "
  •         SELECT *
  •         FROM comedians
  •         WHERE comedian_id = :id;
  •     ";
  •     $stmt = $dbConnection->prepare($sql);
  •     // Run it.
  •     $isWorked = $stmt->execute(['id' => $comedianId]);
  •     if ($isWorked) {
  •         $result = $stmt->fetch();
  •     }
  •     return $result;
  • }

Since I'm likely to want to load comedian entities in other page, I make a function here.

That's the wowkflow finished.

Workflow

Validating fields that are not required

Here's the important code run when the user has entered new comedian data in the form fields:

  1. // Set default values for fields.
  2. $comedianName = '';
  3. $comedianComments = null;
  4. // Is there post data?
  5. if ($_POST) {
  6.     // User filled in data and sent it to this page.
  7.     // Grab the data sent.
  8.     $comedianName = getParamFromPost('name');
  9.     $comedianComments = getParamFromPost('comments');
  10.     // Validate.
  11.     $errorMessage .= checkName($comedianName);
  12.     // Is everything OK?
  13.     if ($errorMessage == '') {
  14.         // No errors.
  15.         // Stash data for processing later.
  16.         $_SESSION['comedian_name'] = $comedianName;
  17.         $_SESSION['comedian_comments'] = $comedianComments;
  18.         // Off to processing.
  19.         header('Location: save-new-comedian.php');
  20.         exit();
  21.     }
  22. }

We have some variables recording name and comments: $comedianName, and $comedianComments.

There's one variable for each field in the comedian record.

First, initialize the variables (lines 10 and 11):

  • $comedianName = '';
  • $comedianComments = null;

Some sort of emptiness value.

Next, fill the variables with data from POST (lines 16 and 17).

  • $comedianName = getParamFromPost('name');
  • $comedianComments = getParamFromPost('comments');

Remember, if POST doesn't have data (the user left the form widget MT), the function getParamFromPost() returns null.

So, after this, $comedianName and $comedianComments have either data the user typed, or null.

Line 19 checks the name.

  • $errorMessage .= checkName($comedianName);

The function checkName() returns an error message, or MT if there is no error.

Now, name is a required field. If the user leaves the name widget MT, that's an error. checkName() checks for that:

  • /**
  •  * @param string $userInput User input to check.
  •  * @return string Error message, MT if data is OK.
  •  */
  • function checkName($userInput) {
  •     $errorMessage = '';
  •     if (is_null($userInput)) {
  •         $errorMessage = 'Sorry, you must enter a name.<br>';
  •     }
  •     return $errorMessage;
  • }

Here's the error message variable.

  • $errorMessage = '';
  • ...
  • $errorMessage .= checkName($wombatName);

$errorMessage starts as MT. checkName() return MT if the data is OK, so...

  • $errorMessage .= ''; MT from checkName()

... leaves $errorMessage MT.

That's why this check of $errorMessage in line 21 makes sense:

  1. // Grab the data sent.
  2. $comedianName = getParamFromPost('name');
  3. ...
  4. // Validate.
  5. $errorMessage .= checkName($comedianName);
  6. // Is everything OK?
  7. if ($errorMessage == '') {
  8.     // No errors.
  9.     // Stash data for processing later.
  10.     ...

That's fine, but what if there's a field that isn't required? Say there's a field for the comedian's weight, but it isn't required. We'll allow the weight field in the DB to be null. Here's how the field is defined in the DB:

Field definition

What about the form? We'll add a weight field:

  • <p>
  •     <label>Weight:
  •         <input type="text" name="weight" value="<?php
  •         if (! is_null($comedianWeight)) {
  •             print $comedianWeight;
  •         }
  •         ?>">
  •     </label>
  • </p>

Now the validation-save-data code. If the user leaves the weight widget in the form MT, we'll put null in the weight field in the DB. But if the user enters anything else, we need to validate.

  • If the user types dog in the weight widget, we want an error message.
  • If the user types -33 in the weight widget, we want an error message.
  • If the user leaves the weight widget MT, we do not want an error message. MT weight is OK.

Here's how we might change the code:

  1. // Set default values for fields.
  2. $comedianName = '';
  3. $comedianWeight = null; New
  4. $comedianComments = null;
  5. // Is there post data?
  6. if ($_POST) {
  7.     // User filled in data and sent it to this page.
  8.     // Grab the data sent.
  9.     $comedianName = getParamFromPost('name');
  10.     $comedianWeight = getParamFromPost('weight'); New
  11.     $comedianComments = getParamFromPost('comments');
  12.     // Validate.
  13.     $errorMessage .= checkName($comedianName);
  14.     // Weight is optional. New
  15.     if (!is_null($comedianWeight)) { New
  16.         $errorMessage .= checkWeight($comedianWeight); New
  17.     } New
  18.     // Is everything OK?
  19.     if ($errorMessage == '') {
  20.         // No errors.
  21.         // Stash data for processing later.
  22.         $_SESSION['comedian_name'] = $comedianName;
  23.         $_SESSION['comedian_weight'] = $comedianWeight; New
  24.         $_SESSION['comedian_comments'] = $comedianComments;
  25.         // Off to processing.
  26.         header('Location: save-new-comedian.php');
  27.         exit();
  28.     }
  29. }

The key is this code:

  • // Validate.
  • $errorMessage .= checkName($comedianName);
  • // Weight is optional.
  • if (!is_null($comedianWeight)) {
  •     $errorMessage .= checkWeight($comedianWeight);
  • }

A null weight is OK, so only do the validation check if the weight is not null.

Exercise

Exercise

Add a wombat

Make a form with validation to add a new wombat to the DB. The form looks like this to start:

MT form

Name is required. The others are optional. Weight must be a number, greater than 0. If there are errors, show the form again, with error messages, and the data the user entered. For example:

Errors

If the data is OK, add the wombat to the DB, and show it, with a link back to the add form.

Output

You can try my solution. The actual save is disabled on mine, but yours should work.

Submit the URL of your solution, and a zip of your files. The usual coding standards apply.

Up next

Let's see how you edit records.