Validation Errors
How validation exceptions flow through the framework to show form errors
The Validation Flow
When a form has invalid data, DALT.PHP uses a special exception to handle it gracefully. Let's trace the complete flow from submission to error display.
The ValidationException Class
class ValidationException extends \Exception
{
public $errors;
public $old;
public static function throw($errors, $old)
{
$instance = new static;
$instance->errors = $errors;
$instance->old = $old;
throw $instance;
}
}Why a Custom Exception?
Regular exceptions:
throw new \Exception("Validation failed");
// Just a message, no structured dataValidationException:
ValidationException::throw(
['email' => 'Invalid email'], // Errors
['email' => 'test@'] // Old input
);
// Carries both errors and old inputThe Static throw() Method
public static function throw($errors, $old)
{
$instance = new static;
$instance->errors = $errors;
$instance->old = $old;
throw $instance;
}Why static?
Convenience. Instead of:
$exception = new ValidationException();
$exception->errors = $errors;
$exception->old = $old;
throw $exception;You can:
ValidationException::throw($errors, $old);What new static means:
- Creates instance of the current class
- Works with inheritance
- Same as
new selfin this case
In the Controller
Example: Registration
// app/Http/controllers/registration/store.php
$email = $_POST['email'];
$password = $_POST['password'];
$errors = [];
// Validate email
if (!Validator::email($email)) {
$errors['email'] = 'Please provide a valid email address';
}
// Validate password
if (!Validator::string($password, 7, 255)) {
$errors['password'] = 'Password must be at least 7 characters';
}
// If errors exist, throw exception
if (!empty($errors)) {
ValidationException::throw($errors, $_POST);
}
// If we reach here, validation passed
// Create the user...
$db = App::resolve(Database::class);
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
$db->query('INSERT INTO users (email, password) VALUES (:email, :password)', [
'email' => $email,
'password' => $hashedPassword
]);
// Log them in
$auth = new Authenticator();
$auth->login(['email' => $email]);
redirect('/');Breaking Down the Validation
1. Get input
$email = $_POST['email'];
$password = $_POST['password'];2. Initialize errors array
$errors = [];3. Validate each field
if (!Validator::email($email)) {
$errors['email'] = 'Please provide a valid email address';
}The Validator class has static methods:
class Validator
{
public static function string($value, $min = 1, $max = INF)
{
$value = trim($value);
return strlen($value) >= $min && strlen($value) <= $max;
}
public static function email($value)
{
return filter_var($value, FILTER_VALIDATE_EMAIL);
}
}4. Throw if errors exist
if (!empty($errors)) {
ValidationException::throw($errors, $_POST);
}Why pass $_POST as old input?
- So the form can be pre-filled
- User doesn't have to retype everything
- Better UX
5. Continue if valid
If no exception is thrown, the code continues and creates the user.
In the Entry Point
Remember from Part 1.4, public/index.php catches this exception:
try {
$router->route($uri, $method, $request);
} catch (ValidationException $exception) {
Session::flash('errors', $exception->errors);
Session::flash('old', $exception->old);
redirect($router->previousUrl());
} catch (\Throwable $e) {
app_log(get_class($e) . ': ' . $e->getMessage());
throw $e;
}The Catch Block
1. Flash the errors
Session::flash('errors', $exception->errors);Stores:
$_SESSION['_flash']['errors'] = [
'email' => 'Please provide a valid email address',
'password' => 'Password must be at least 7 characters'
];2. Flash the old input
Session::flash('old', $exception->old);Stores:
$_SESSION['_flash']['old'] = [
'email' => 'test@',
'password' => 'short'
];3. Redirect back
redirect($router->previousUrl());What previousUrl() does:
public function previousUrl()
{
return $this->request?->server('HTTP_REFERER')
?? $_SERVER['HTTP_REFERER']
?? '/';
}Reads the Referer header (the page you came from).
Example:
User is on: GET /register
Submits to: POST /register
Referer: /register
Redirects: /registerIn the View
Displaying Errors
// resources/views/registration/create.view.php
<?php
$errors = Session::get('errors', []);
?>
<form method="POST" action="/register">
<?= csrf_field() ?>
<div>
<label>Email</label>
<input
type="email"
name="email"
value="<?= htmlspecialchars(old('email')) ?>"
required
>
<?php if (isset($errors['email'])): ?>
<p class="error"><?= htmlspecialchars($errors['email']) ?></p>
<?php endif; ?>
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
value="<?= htmlspecialchars(old('password')) ?>"
required
>
<?php if (isset($errors['password'])): ?>
<p class="error"><?= htmlspecialchars($errors['password']) ?></p>
<?php endif; ?>
</div>
<button type="submit">Register</button>
</form>Breaking Down the View
1. Get errors from flash
$errors = Session::get('errors', []);Returns the errors array or empty array if none.
2. Pre-fill input with old value
value="<?= htmlspecialchars(old('email')) ?>"The old() helper:
function old($key, $default = '')
{
return Core\Session::get('old')[$key] ?? $default;
}3. Show error if exists
<?php if (isset($errors['email'])): ?>
<p class="error"><?= htmlspecialchars($errors['email']) ?></p>
<?php endif; ?>Why htmlspecialchars()?
Security. Prevents XSS attacks:
// Malicious input
$email = '<script>alert("XSS")</script>';
// Without htmlspecialchars
echo $email; // Executes script!
// With htmlspecialchars
echo htmlspecialchars($email);
// Outputs: <script>alert("XSS")</script>
// Browser shows it as text, doesn't executeThe Complete Flow Diagram
1. User fills form
↓
2. POST /register
↓
3. Controller validates
↓
4. Validation fails
↓
5. ValidationException::throw($errors, $old)
↓
6. Exception bubbles up
↓
7. Caught in public/index.php
↓
8. Session::flash('errors', $errors)
Session::flash('old', $old)
↓
9. redirect($previousUrl)
↓
10. GET /register
↓
11. View reads flash data
↓
12. Errors displayed
Old input pre-filled
↓
13. User fixes and resubmits
↓
14. Validation passes
↓
15. User created
↓
16. Redirect to dashboardMultiple Field Validation
Building the Errors Array
$errors = [];
// Email validation
if (!$_POST['email']) {
$errors['email'] = 'Email is required';
} elseif (!Validator::email($_POST['email'])) {
$errors['email'] = 'Email must be valid';
}
// Password validation
if (!$_POST['password']) {
$errors['password'] = 'Password is required';
} elseif (!Validator::string($_POST['password'], 7, 255)) {
$errors['password'] = 'Password must be at least 7 characters';
}
// Password confirmation
if ($_POST['password'] !== $_POST['password_confirmation']) {
$errors['password_confirmation'] = 'Passwords must match';
}
// Terms acceptance
if (!isset($_POST['terms'])) {
$errors['terms'] = 'You must accept the terms';
}
if (!empty($errors)) {
ValidationException::throw($errors, $_POST);
}Result:
$errors = [
'email' => 'Email must be valid',
'password' => 'Password must be at least 7 characters',
'password_confirmation' => 'Passwords must match',
'terms' => 'You must accept the terms'
];Error Display Patterns
Inline Errors (Next to Field)
<input name="email" value="<?= old('email') ?>">
<?php if (isset($errors['email'])): ?>
<span class="error"><?= $errors['email'] ?></span>
<?php endif; ?>Error Summary (Top of Form)
<?php if (!empty($errors)): ?>
<div class="error-summary">
<h3>Please fix the following errors:</h3>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>Field Highlighting
<input
name="email"
value="<?= old('email') ?>"
class="<?= isset($errors['email']) ? 'input-error' : '' ?>"
>CSS:
.input-error {
border-color: red;
background-color: #fee;
}Why This Pattern Works
1. Separation of Concerns
- Controller: Validates and throws
- Entry point: Catches and flashes
- View: Displays
Each layer has one job.
2. Consistent Error Handling
All validation errors flow through the same path:
- Always flashed
- Always redirected back
- Always displayed the same way
3. Good UX
- User sees exactly what's wrong
- Form is pre-filled (no retyping)
- Errors disappear after fixing
4. Security
htmlspecialchars()prevents XSS- Errors are temporary (flash)
- No sensitive data in URLs
Key Takeaways
- ValidationException carries data - Errors and old input
- Thrown in controllers - When validation fails
- Caught in entry point - Flashed and redirected
- Displayed in views - With old input pre-filled
- Automatic cleanup - Flash data deleted after display
What's Good Here
✅ Clean separation of concerns
✅ Consistent error handling
✅ Great user experience
✅ Secure (XSS prevention)
✅ Simple enough to understand and extend
Design Note
Validation rule objects and automatic validation are intentionally omitted. DALT keeps validation explicit - you write the checks yourself and see exactly what's being validated. This teaches you what validation frameworks like Laravel's Validator are doing behind the scenes.
Next, explore authentication and how login/logout works.
This would require a more sophisticated validation system.
Part 4 Complete!
You now understand sessions and flash data:
- How the Session class wraps $_SESSION
- How flash data works for temporary messages
- How validation errors flow through the framework
- How forms display errors and pre-fill input