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:
Three 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.
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:
The user first sees a form to fill in, with a Save button (1).
We know how to do that.
They type in their data, and click save (2). That POSTs the data back to the page, which reloads itself.
Here's the code.
- <?php
- /**
- * Create/edit comedian.
- * This page validates, as well as showing the form.
- */
- session_start();
- require_once 'library/useful-functions.php';
- $errorMessage = '';
- // Set default values for fields.
- $comedianName = '';
- $comedianComments = null;
- // Is there post data?
- if ($_POST) {
- // User filled in data and sent it to this page.
- // Grab the data sent.
- $comedianName = getParamFromPost('name');
- $comedianComments = getParamFromPost('comments');
- // Validate.
- $errorMessage .= checkName($comedianName);
- // Is everything OK?
- if ($errorMessage == '') {
- // No errors.
- // Stash data for processing later.
- $_SESSION['comedian_name'] = $comedianName;
- $_SESSION['comedian_comments'] = $comedianComments;
- // Off to processing.
- header('Location: save-new-comedian.php');
- exit();
- }
- }
- ?><!doctype html>
- <html lang="en">
- <head>
- <?php
- $pageTitle = 'Add comedian';
- require_once 'library/page-components/head.php';
- ?>
- </head>
- <body>
- <?php
- require_once 'library/page-components/top.php';
- print "<h1>$pageTitle</h1>\n";
- if ($errorMessage != '') {
- print "<p class='error-message'>$errorMessage</p>\n";
- }
- ?>
- <form method="post">
- <p>
- <label>Name:
- <input type="text" name="name" value="<?php
- if (! is_null($comedianName)) {
- print $comedianName;
- }
- ?>">
- <input type="text" name="name" value="<?php print $comedianName; ?>">
- </label>
- </p>
- <p>
- <label>Comments:
- <textarea name="comments"><?php
- if (! is_null($comedianComments)) {
- print $comedianComments;
- }
- ?></textarea>
- </label>
- </p>
- <p>
- <button type="submit">Save</button>
- </p>
- </form>
- <?php
- require_once 'library/page-components/footer.php';
- ?>
- </body>
- </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
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
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.
Here's code for the processing page:
- <?php
- session_start();
- // Connect to the database.
- require_once 'library/db-connect.php';
- // Get the data to save.
- $name = $_SESSION['comedian_name'];
- $comments = $_SESSION['comedian_comments'];
- // Save the data into the DB.
- $sql = "
- INSERT INTO comedians (
- name, comments
- )
- VALUES (
- :name, :comments
- )
- ";
- /** @var PDO $dbConnection */
- $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");
- 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:
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
.
Why doesn't the SQL INSERT give a value for comedian_id
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:
After making the new comedian entity, we want to show it. Like this:
The id is passed at the end of the URL. Here's the code.
- <?php
- require_once 'library/useful-stuff.php';
- $errorMessage = '';
- // Get id from URL.
- $comedianId = getParamFromGet('comedian_id');
- $errorMessage = checkComedianId($comedianId);
- if ($errorMessage == '') {
- $comedianEntity = getComedianWithId($comedianId);
- if (is_null($comedianEntity)) {
- $errorMessage = "Sorry, problem loading comedian with id $comedianId.";
- }
- else {
- // Put the field values into variables.
- $comedianName = $comedianEntity['name'];
- $comedianComments = $comedianEntity['comments'];
- }
- }
- ?><!doctype html>
- <html lang="en">
- <head>
- <?php
- $pageTitle = 'Comedian';
- require_once 'library/page-components/head.php';
- ?>
- </head>
- <body>
- <div class="container">
- <?php
- require_once 'library/page-components/top.php'
- ?>
- <h1>Comedian</h1>
- <?php
- if ($errorMessage != '') {
- print "<p class='error-message'>$errorMessage</p>";
- }
- else {
- print "<p>Name: $comedianName</p>\n";
- print "<p>Comments: $comedianComments</p>\n";
- }
- require_once 'library/page-components/footer.php';
- ?>
- </body>
- </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.
Validating fields that are not required
Here's the important code run when the user has entered new comedian data in the form fields:
- // Set default values for fields.
- $comedianName = '';
- $comedianComments = null;
- // Is there post data?
- if ($_POST) {
- // User filled in data and sent it to this page.
- // Grab the data sent.
- $comedianName = getParamFromPost('name');
- $comedianComments = getParamFromPost('comments');
- // Validate.
- $errorMessage .= checkName($comedianName);
- // Is everything OK?
- if ($errorMessage == '') {
- // No errors.
- // Stash data for processing later.
- $_SESSION['comedian_name'] = $comedianName;
- $_SESSION['comedian_comments'] = $comedianComments;
- // Off to processing.
- header('Location: save-new-comedian.php');
- exit();
- }
- }
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:
- // Grab the data sent.
- $comedianName = getParamFromPost('name');
- ...
- // Validate.
- $errorMessage .= checkName($comedianName);
- // Is everything OK?
- if ($errorMessage == '') {
- // No errors.
- // Stash data for processing later.
- ...
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:
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:
- // Set default values for fields.
- $comedianName = '';
- $comedianWeight = null; New
- $comedianComments = null;
- // Is there post data?
- if ($_POST) {
- // User filled in data and sent it to this page.
- // Grab the data sent.
- $comedianName = getParamFromPost('name');
- $comedianWeight = getParamFromPost('weight'); New
- $comedianComments = getParamFromPost('comments');
- // Validate.
- $errorMessage .= checkName($comedianName);
- // Weight is optional. New
- if (!is_null($comedianWeight)) { New
- $errorMessage .= checkWeight($comedianWeight); New
- } New
- // Is everything OK?
- if ($errorMessage == '') {
- // No errors.
- // Stash data for processing later.
- $_SESSION['comedian_name'] = $comedianName;
- $_SESSION['comedian_weight'] = $comedianWeight; New
- $_SESSION['comedian_comments'] = $comedianComments;
- // Off to processing.
- header('Location: save-new-comedian.php');
- exit();
- }
- }
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
Add a wombat
Make a form with validation to add a new wombat to the DB. The form looks like this to start:
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:
If the data is OK, add the wombat to the DB, and show it, with a link back to the add form.
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.