📖 Exceptions and Error Handling

In the realm of web application development, ensuring a seamless and error-free user experience is paramount. PHP, with its rich set of features for Object-Oriented Programming (OOP), provides a robust framework for handling unexpected situations through Exceptions and Error Handling. This article delves into the best practices for incorporating these mechanisms within the MVC (Model-View-Controller) architecture to enhance application reliability and maintainability.

The Essence of Error Handling

At the heart of resilient application development is effective error handling. In PHP, errors can disrupt the normal flow of an application, leading to a poor user experience. However, PHP's Exception handling mechanism offers a more graceful way to deal with such unforeseen situations. Unlike traditional error handling that uses conditional statements and functions like `error_reporting()`, Exceptions provide a structured approach to identifying, managing, and recovering from errors.

Understanding Exceptions

An Exception in PHP is a standard object that represents an error or an unexpected condition. It can be "thrown" from a point in the application where a problem occurs and "caught" in another part where the application can take appropriate action. This mechanism is encapsulated using `try`, `catch`, and `finally` blocks.

<?php
try {
    // Code that may throw an exception
} catch (Exception $e) {
    // Code to handle the exception
} finally {
    // Code that will always execute, regardless of an exception being thrown or not
}

Exceptions in MVC Architecture

In an MVC application, exceptions can be strategically used across different layers (Model, View, Controller) to handle errors effectively.

Model Layer

The Model is responsible for data handling and business logic. Here, exceptions are invaluable for managing database errors, validation errors, or any data-related issues.

Custom Database Exception Example
<?php
class DatabaseException extends Exception {}

function fetchData($query) {
    try {
        // Attempt database operation
        throw new DatabaseException("Failed to fetch data.");
    } catch (DatabaseException $e) {
        // Handle database-specific errors here
        echo $e->getMessage();
    }
}

Controller Layer

Controllers handle the application's request-response cycle. Exceptions in controllers can simplify error handling, especially for validation or processing errors.

Invalid Input Exception Example
<?php
class InputException extends Exception {}

function processRequest($data) {
    try {
        if (!isValid($data)) {
            throw new InputException("Invalid input provided.");
        }
        // Proceed with processing
    } catch (InputException $e) {
        // Handle invalid input
        echo $e->getMessage();
    }
}

View Layer

While less common, exceptions can occur in the view layer, primarily related to rendering issues. However, it's generally advisable to catch exceptions before they propagate to the view.

Creating Custom Exceptions

Extending the base Exception class allows for more specific error handling. This promotes clearer and more maintainable code.

Custom Exception Example

<?php
class UserNotFoundException extends Exception {}

function findUser($userId) {
    try {
        // Attempt to find user
        throw new UserNotFoundException("User not found.");
    } catch (UserNotFoundException $e) {
        // Handle user not found scenario
        echo $e->getMessage();
    }
}

Best Practices

Use Exceptions for Exceptional Conditions
Avoid using exceptions for controlling application flow. They should be reserved for truly unexpected situations.
Global Exception Handling
Implement a global exception handler to catch and manage any unhandled exceptions, preventing application crashes.
Logging
Always log exceptions. This aids in debugging and monitoring the health of your application.

Creating Custom Exceptions

Incorporating namespaces and `use` statements is key for organizing code and managing dependencies in modern PHP applications, especially within an MVC framework. Let's revise the section on creating custom exceptions to include these concepts, specifically focusing on a `PageNotFoundException` class.

When working with a PHP MVC framework, organizing your classes into namespaces can greatly enhance the modularity and clarity of your application. Let's look at how to define a custom exception class within a namespace and how to use it within the application.

PageNotFoundException Class

First, let's define the `PageNotFoundException` class within the `Framework\Exceptions` namespace. This class will extend PHP's `DomainException` class, which is a built-in exception type designed for representing domain-related errors. The `DomainException` class is part of the global namespace, so we'll need to import it using a `use` statement.

PageNotFoundException.php
<?php

namespace Framework\Exceptions;

use DomainException;

class PageNotFoundException extends DomainException
{
    // Custom functionality or properties for the exception can be added here
}

This code snippet demonstrates how to declare a namespace and import another class into the current scope. By extending `DomainException`, `PageNotFoundException` becomes a specialized exception type that can be used throughout your application to signal that a requested page could not be found.

PageNotFoundException

Now, let's see how you might use this custom exception within your MVC application. Imagine you have a controller method responsible for fetching and displaying a page. If the page cannot be found, you would throw a `PageNotFoundException`.

Example
<?php

namespace App\Controllers;

use Framework\Exceptions\PageNotFoundException;

class PageController
{
    public function show($pageId)
    {
        try {
            $page = $this->findPageById($pageId);

            if (!$page) {
                throw new PageNotFoundException("The page with ID {$pageId} was not found.");
            }

            // Render the page
        } catch (PageNotFoundException $e) {
            // Handle the exception, for example, by showing a 404 error page
            echo $e->getMessage();
        }
    }

    private function findPageById($id)
    {
        // Logic to find the page by ID
        // Return null if not found
    }
}

In this example, the `PageController` is part of the `App\Controllers` namespace and it uses the `PageNotFoundException` from the `Framework\Exceptions` namespace. When the `findPageById` method fails to find the page (i.e., returns `null`), a `PageNotFoundException` is thrown. The catch block then handles this exception, perhaps by displaying a 404 error page to the user.

General Site Use

To catch most of the site issues that relate to class errors, page not found errors and malformed URL errors, you should add this functionality to the top of the main index.php file where it can evaluate and respond to these errors while still processing the page. Try adding the following code to the index.php page.

<?php

declare(strict_types=1);

use Framework\Exceptions\PageNotFoundException;

// Catches error information then passes it to the ErrorException function
set_error_handler(function(
    int $errno,
    string $errstr,
    string $errfile,
    int $errline
): bool
{
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

// Calls the custom exception handler
set_exception_handler(function (Throwable $exception) {

    if ($exception instanceof Framework\Exceptions\PageNotFoundException) {

        http_response_code(404);

        $template = "404.php";

    } else {
    
        http_response_code(500);

        $template = "500.php";

    }

    $show_errors = false;

    if ($show_errors) {

        ini_set("display_errors", "1");

        require "views/$template";

    } else {

        ini_set("display_errors", "0");

        ini_set("log_errors", "1");

        require "views/$template";

    }

    throw $exception;

});

This should catch errors that throw exceptions, sort them by type and then display a custom error page for the user. Modify the following code farther down the index.php file to check for non-existent (unmatched) routes.

<?php
...
$params = $router->matchRoute($path);

// remove existing response to non-existent (unmatched) routes
if ($params === false) {

    exit("No matching route");
    
}

// add modified code to throw a page not found exception
if ($params === false) {

    throw new PageNotFoundException("No matching route for '$path'.");
    
}
...

Error Page Views

The error page views (500.php and 404.php) should be placed in the views directory and called so the minimum amount of code must be processed since you don't know what caused the error. For this purpose we will add a new directory in the views directory to hold our error pages. These pages will be complete with header so we won't need to call the header.php view. Again, we don't know what caused the error or how much functionality our site still has.

Error Page Example

This code shows the 404.php page, but the 500.php page is similar with minor edits.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Page Not Found</title>
</head>
<body>
    <h1>Error 404.</h1>
    <h2>Page Not Found.</h2>
    <p>Go back to the <a href="/">Home page</a>.</p>
</body>
</html>

File System

The new file system should look like the examples below.

Sample File Directory Structure

File Structure

↧ ERROR HANDLING (SITE ROOT)
↳ src
| ↳ App
| | ↳ Controllers
| | | ↳ Home.php
| | | ↳ Products.php
| | ↳ Models
| | | ↳ Product.php
| ↳ Framework
| | ↳ Exceptions
| | | ↳ PageNotFoundException.php
| | | ↳ Router.php
| | | ↳ Viewer.php
↳ views
| ↳ Home
| | ↳ index.php
| ↳ Products
| | ↳ index.php
| | ↳ show.php
| ↳ shared
| | ↳ header.php
| ↳ 404.php
| ↳ 500.php
↳ .htaccess
↳ index.php