Task 5 – Server-Side Form Validation (PHP)
Project Files
The starter HTML already has the form and placeholder #success / #errors divs. Your job is to add PHP at the top to process the submitted data and conditionally show/hide the divs.
<?php
require_once 'nations.php';
// ✏️ Add your validation logic here
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Task 5.</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<h1>5. New music</h1>
<div id="main">
<form method="post">
<label>Title <input name="title"></label>
<label>Release year <input name="year"></label>
<label>Views <input name="views"></label>
<div>
Manual ID
<label><input type="radio" name="manualid" value="yes"> Yes</label>
<label><input type="radio" name="manualid" value="no"> No</label>
</div>
<label>ID <input name="id"></label>
<input type="submit">
</form>
<div id="success">New song added!</div>
<div id="errors">
Error!
<ul><li>Example error.</li></ul>
</div>
</div>
</body>
</html>Add require '../data/data_array_of_objects.php' alongside nations.php for the ID uniqueness check. Initialize $errors = [] and $success = false before the form-processing block.
Read title, year, and views from $_POST (trim each). If any is empty after trimming, add an error message for it.
$_POST['key'] ?? '' uses the null coalescing operator — if the field is absent from the request, it returns '' instead of triggering an “undefined index” notice. Always use ?? '' when reading form fields.
trim() removes leading and trailing whitespace. Without it, a user typing only spaces would pass the empty-string check. Always trim before validating text inputs.
$errors[] = '...' appends to the array — shorthand for array_push. This design collects all errors before displaying them, so the user sees every problem at once.
$title = trim($_POST['title'] ?? '');
$year = trim($_POST['year'] ?? '');
$views = trim($_POST['views'] ?? '');
if ($title === '') $errors[] = 'Title is required.';
if ($year === '') $errors[] = 'Year is required.';
if ($views === '') $errors[] = 'Views is required.'; If title is shorter than 5 characters, add an error.
strlen() returns the byte count of a string, which equals the character count for ASCII. For multibyte Unicode text (e.g. Hungarian), mb_strlen() is more correct — but strlen() is accepted for exam purposes.
This check runs even when $title is empty (which already triggered an error in subtask a). Both errors will accumulate in $errors and be displayed together, which is the intended behavior.
if (strlen($title) < 5) {
$errors[] = 'Title must be at least 5 characters.';
} If the title does not contain the substring - (space-dash-space), add an error.
strpos($haystack, $needle) returns the character index of the first occurrence, or false if not found. You must use === false (strict) — not == false — because position 0 is also falsy. A loose == false check would incorrectly reject titles that begin with -.
This is one of PHP’s classic gotchas: always use === false or !== false when checking strpos results.
if (strpos($title, ' - ') === false) {
$errors[] = 'Title must contain " - " to separate artist and song.';
} If year is not a valid integer, add an error.
filter_var($value, FILTER_VALIDATE_INT) returns the integer on success or false on failure. It rejects floats, non-numeric strings, and empty values. The strict === false comparison is again necessary since the return value is either an integer or false.
Alternative: ctype_digit($year) checks that every character is a digit — simpler but rejects negative numbers. FILTER_VALIDATE_INT handles edge cases like "-2024" correctly.
if (filter_var($year, FILTER_VALIDATE_INT) === false) {
$errors[] = 'Year must be a whole number.';
} If views is not numeric, add an error.
is_numeric($string) returns true for integers, floats, and numeric strings like "3.14" or "1e5". This is the right choice for a views field that may contain decimal millions.
Unlike FILTER_VALIDATE_INT, it accepts floats — which is appropriate since views are stored as decimal numbers (e.g. 9.5 million).
if (!is_numeric($views)) {
$errors[] = 'Views must be a number.';
} Read the manualid radio value from $_POST. Only validate the id field when the value is "yes".
HTML radio buttons submit the selected value attribute. If neither radio is selected (or “no” is chosen), only the ID validation block is skipped — the rest of the form still validates.
The ?? '' fallback handles the case where neither radio is checked (the field won’t appear in $_POST at all), giving an empty string that is not "yes".
$manualid = $_POST['manualid'] ?? '';
if ($manualid === 'yes') {
// id validation goes here (subtasks g, h, i)
} Inside the manualid === 'yes' block: if id is empty after trimming, add an error.
The else branch is important: format and uniqueness checks only make sense when $id is non-empty. Without the else, preg_match would run on an empty string and produce misleading errors alongside the “required” error.
if ($manualid === 'yes') {
$id = trim($_POST['id'] ?? '');
if ($id === '') {
$errors[] = 'ID is required when entering manually.';
} else {
// format and uniqueness checks (subtasks h, i)
}
} Inside the else branch: if the submitted id already exists in $data, add an error.
array_filter($array, $callback) returns a new array keeping only elements where the callback returns true. It preserves original keys — the result might not be sequential, but count() still works correctly.
This is PHP’s equivalent of JavaScript’s array.some() pattern (filter + count > 0 since PHP has no built-in any()).
// Requires: require '../data/data_array_of_objects.php'; at top
$existing = array_filter($data, fn($v) => $v->id === $id);
if (count($existing) > 0) {
$errors[] = 'A video with this ID already exists.';
} In the else branch: if id does not match 8 lowercase hexadecimal characters, add an error.
Breaking down /^[0-9a-f]{8}$/:
^— start of string anchor[0-9a-f]— any hex digit (digits 0–9 or letters a–f, lowercase){8}— exactly 8 repetitions$— end of string anchor
The anchors are critical — without ^ and $, a 10-character string containing 8 valid hex chars somewhere in the middle would match. The anchors force the entire string to match.
preg_match() returns 1 (match), 0 (no match), or false (error). Negating with ! treats both 0 and false as “invalid.”
if (!preg_match('/^[0-9a-f]{8}$/', $id)) {
$errors[] = 'ID must be exactly 8 lowercase hex characters.';
} After a failed submission, re-populate each form field with the value the user typed, so they don’t have to re-enter valid data.
Initializing $title = $year = $views = $id = $manualid = '' before the if ($_POST) block ensures the variables exist even on the first load (before any form submission), preventing “undefined variable” notices in the template.
htmlspecialchars() on output prevents XSS — if the user typed "><script>, it becomes harmless "><script> in the HTML attribute.
For radio buttons, the checked attribute is added conditionally: comparing $manualid to each radio’s value attribute. The ternary outputs 'checked' (a bare HTML attribute) or an empty string.
<!-- Initialize variables before the if ($_POST) block: -->
<?php $title = $year = $views = $id = $manualid = ''; ?>
<!-- In the form inputs: -->
<input name="title" value="<?= htmlspecialchars($title) ?>">
<input name="year" value="<?= htmlspecialchars($year) ?>">
<input name="views" value="<?= htmlspecialchars($views) ?>">
<input name="id" value="<?= htmlspecialchars($id) ?>">
<!-- Radio stays checked: -->
<input type="radio" name="manualid" value="yes"
<?= $manualid === 'yes' ? 'checked' : '' ?>>
<input type="radio" name="manualid" value="no"
<?= $manualid === 'no' ? 'checked' : '' ?>> After all validation: set $success = true when there are no errors. In the HTML, show either #success or #errors (with the error list), hiding whichever is not relevant.
count($errors) === 0 is equivalent to empty($errors) for arrays — use whichever reads more clearly.
The starter HTML has both #success and #errors divs visible. Replace them with this conditional block so only the relevant one is rendered. The elseif (!empty($errors)) guard means neither block appears on the initial page load (before any POST).
// At the end of if ($_POST):
if (count($errors) === 0) $success = true;<!-- In the HTML: -->
<?php if ($success): ?>
<div id="success">New song added!</div>
<?php elseif (!empty($errors)): ?>
<div id="errors">
Error!
<ul>
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?> Complete Solution
<?php
require_once 'nations.php';
require '../data/data_array_of_objects.php';
$errors = [];
$success = false;
$title = $year = $views = $id = $manualid = '';
if ($_POST) {
$title = trim($_POST['title'] ?? '');
$year = trim($_POST['year'] ?? '');
$views = trim($_POST['views'] ?? '');
$manualid = $_POST['manualid'] ?? '';
// a: required fields
if ($title === '') $errors[] = 'Title is required.';
if ($year === '') $errors[] = 'Year is required.';
if ($views === '') $errors[] = 'Views is required.';
// b: title length
if (strlen($title) < 5) $errors[] = 'Title must be at least 5 characters.';
// c: title format
if (strpos($title, ' - ') === false) $errors[] = 'Title must contain " - ".';
// d: year integer
if (filter_var($year, FILTER_VALIDATE_INT) === false) $errors[] = 'Year must be a whole number.';
// e: views numeric
if (!is_numeric($views)) $errors[] = 'Views must be a number.';
// f-i: manual ID
if ($manualid === 'yes') {
$id = trim($_POST['id'] ?? '');
if ($id === '') {
$errors[] = 'ID is required when entering manually.';
} else {
// h: uniqueness
$existing = array_filter($data, fn($v) => $v->id === $id);
if (count($existing) > 0) $errors[] = 'A video with this ID already exists.';
// i: format
if (!preg_match('/^[0-9a-f]{8}$/', $id)) $errors[] = 'ID must be 8 lowercase hex characters.';
}
}
// k: set success
if (count($errors) === 0) $success = true;
}
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Task 5.</title><link rel="stylesheet" href="index.css"></head>
<body>
<h1>5. New music</h1>
<div id="main">
<?php if ($success): ?>
<div id="success">New song added!</div>
<?php elseif (!empty($errors)): ?>
<div id="errors">
Error!
<ul>
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="post">
<label>Title <input name="title" value="<?= htmlspecialchars($title) ?>"></label>
<label>Release year <input name="year" value="<?= htmlspecialchars($year) ?>"></label>
<label>Views <input name="views" value="<?= htmlspecialchars($views) ?>"></label>
<div>
Manual ID
<label><input type="radio" name="manualid" value="yes" <?= $manualid === 'yes' ? 'checked' : '' ?>> Yes</label>
<label><input type="radio" name="manualid" value="no" <?= $manualid === 'no' ? 'checked' : '' ?>> No</label>
</div>
<label>ID <input name="id" value="<?= htmlspecialchars($id) ?>"></label>
<input type="submit">
</form>
</div>
</body>
</html>