LEARN TO CODE | PHP | 04 MINI PROJECTS AND NEXT STEPS

04.1 Error handling and configuration

Separate mistakes in the program, invalid input and service failures, then decide what the user and developer each need to see.

01 | WHEN A WORKING PAGE STOPS WORKING

Your directory worked yesterday. Today the browser shows a blank page. Perhaps a semicolon is missing; perhaps the database is switched off. These look similar from the browser, but they need different fixes.

Error handling means deciding what should happen when an operation cannot complete. Configuration means keeping changeable settings, such as the database host, separate from the code that uses them. Together, they make a growing project easier to understand and recover.

Use PHP 8.1 or later with PDO MySQL and a running local MariaDB/MySQL server. The PHP built-in server does not install or start the database. Download the complete Section 04 example files , extract them and open the php04-examples folder in VS Code.

The public folder contains browser-facing pages. The private and tools folders are siblings outside that document root. Keep the complete folder structure: copying individual PHP pages without their shared files will not work.

02 | THREE DIFFERENT PROBLEMS

Different problems need different responses
Situation What it means Appropriate response
Missing closing bracket Syntax/parse error: PHP cannot understand source Fix the source; inspect development diagnostics
Visitor leaves a required field blank Expected validation failure Show a clear field message and preserve safe form values
Database connection is unavailable Operational failure Avoid claiming success; show a general failure and investigate logs

Do not use a catch block as a replacement for ordinary input checks. An empty name is a normal possibility in a form; a database becoming unavailable is a different kind of event. A logic error can also produce the wrong answer without triggering any PHP diagnostic.

03 | READ A DIAGNOSTIC USEFULLY

Start with the error type, the message, then the filename and line. An error reported at a closing brace may have been caused by a missing semicolon on the previous line. Change one thing, save and test again.

<?php
$capacity = 12
echo $capacity;

This deliberately broken fragment is missing the semicolon after 12. PHP cannot parse it successfully. Do not add it to the shared example project; use a separate scratch file and fix it before continuing.

TRY IT YOURSELF

Why would changing the database password not fix this example?

Reveal guidance

The parser cannot understand the PHP source, so it has not reached a database operation. Fix the syntax first.

04 | DEVELOPMENT SETTINGS AND SERVER LOGS

development.ini

error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
; bootstrap.php deliberately hides diagnostics on the project response.
; Use the terminal log to investigate handled/unhandled project failures.

In the VS Code terminal, from the project root, start the server once:

php -c development.ini -S 127.0.0.1:8000 -t public

Keep this terminal running. Hover over http://127.0.0.1:8000 and follow the link, then add the required filename in the browser. Save PHP changes in VS Code and refresh the browser. You do not need to restart the server for every source edit; configuration changes may require a restart.

Use this local PHP server rather than a static Live Server extension. The browser must request a page through PHP for server-side code to execute.

display_errors controls whether diagnostics are printed in the response. log_errors controls logging. error_reporting selects which levels are reported; E_ALL requests all reporting levels available in that runtime.

The project bootstrap deliberately turns browser display off so the examples consistently return controlled error messages. Read the VS Code server terminal for diagnostics. In an isolated syntax scratch file without that bootstrap, the development settings may display the parse error as well.

Runtime ini_set calls cannot fix a parse error in the same file, because that file never begins running. Server-level PHP configuration matters for errors that happen before your code executes.

05 | KEEP CONFIGURATION PRIVATE

private/config.example.php

<?php
return [
    'host' => '127.0.0.1',
    'port' => 3306,
    'database' => 'php_projects',
    'username' => 'projects_app',
    'password' => 'REPLACE_WITH_LOCAL_DATABASE_PASSWORD',
    'secure_cookies' => false, // Local HTTP only; use true with HTTPS.
];

Copy this example to config.php locally. The example can be shared; the completed file containing your password should be excluded from source control and shared downloads.

A folder named private is not automatically protected. Here protection comes from serving only public with -t public. On a different host, confirm the actual document root and place secrets outside it. Environment-managed secrets are another option in deployed systems.

secure_cookies is false only for local HTTP. A real HTTPS deployment should use Secure cookies and a suitable server configuration. Do not select an insecure mode from a URL query parameter.

06 | LET PDO REPORT A CONNECTION FAILURE

private/database.php

<?php
function openDatabase(): PDO
{
    $c = require __DIR__ . '/config.php';
    return new PDO(
        'mysql:host=' . $c['host'] . ';port=' . $c['port']
        . ';dbname=' . $c['database'] . ';charset=utf8mb4',
        $c['username'], $c['password'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );
}

openDatabase returns a PDO connection on success. PDO’s exception mode means failures can interrupt the normal flow instead of allowing the program to continue as if the operation succeeded.

The connection function does not print a password, echo a database error or return a pretend empty result. Its caller can decide how to turn a failure into an HTTP response.

07 | UNDERSTAND TRY, THROW AND CATCH

<?php
try {
    $db = openDatabase();
    $count = $db->query('SELECT COUNT(*) FROM Resources')->fetchColumn();
    echo 'Resources: ' . (int)$count;
} catch (PDOException $error) {
    error_log('Resource count query failed.');
    http_response_code(500);
    echo 'The resource count is temporarily unavailable.';
}

This fragment assumes database.php has already been required. try encloses an operation that may throw; catch handles the named exception type. When the exception occurs, PHP skips the remaining statements in that try block and enters the matching handler.

throw creates or forwards an exception. It is useful when a function cannot fulfil its contract, such as returning a connection. A catch that handles PDOException does not automatically catch every possible programming error.

08 | THE PROJECT’S SHARED BOOTSTRAP

private/bootstrap.php

<?php
require_once __DIR__ . '/database.php';
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
set_exception_handler(function (Throwable $error): void {
    // Do not log submitted fields, passwords or database error messages.
    error_log('Unhandled ' . get_class($error) . ' at '
        . basename($error->getFile()) . ':' . $error->getLine());
    http_response_code(500);
    echo 'This request could not be completed. Please try again later.';
});
$config = require __DIR__ . '/config.php';
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_name('php04session');
session_set_cookie_params([
    'lifetime' => 0, 'path' => '/',
    'secure' => (bool)$config['secure_cookies'],
    'httponly' => true, 'samesite' => 'Lax',
]);
if (!session_start()) {
    throw new RuntimeException('Session storage unavailable');
}
header('Cache-Control: no-store');
if (!isset($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}
function h(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function postText(string $key): string
{
    $value = $_POST[$key] ?? '';
    return is_string($value) ? trim($value) : '';
}
function checkCsrf(): void
{
    $value = $_POST['csrf'] ?? null;
    if (!is_string($value) || !hash_equals($_SESSION['csrf'], $value)) {
        http_response_code(403);
        exit('Form expired or invalid. Reload the page.');
    }
}
function redirect(string $localPath): never
{
    // All callers supply fixed local paths, never a submitted redirect URL.
    header('Location: ' . $localPath, true, 303);
    exit;
}
function currentUser(PDO $db): ?array
{
    $id = $_SESSION['user_id'] ?? null;
    if (!is_int($id)) return null;
    $q = $db->prepare('SELECT user_id, username, role FROM Users WHERE user_id = :id');
    $q->execute(['id' => $id]);
    $user = $q->fetch();
    if ($user === false) {
        unset($_SESSION['user_id']);
        return null;
    }
    return $user;
}
function requireUser(PDO $db): array
{
    $user = currentUser($db);
    if ($user === null) redirect('/login.php');
    return $user;
}
function requireOwner(array $user, array $resource): void
{
    if ($user['role'] !== 'editor' || (int)$resource['owner_id'] !== (int)$user['user_id']) {
        http_response_code(403);
        exit('You cannot change this resource.');
    }
}

A bootstrap is the shared starting code required before each page outputs HTML. This one loads helpers, configures sessions, creates a CSRF token and installs a final handler for uncaught Throwables. Later lessons explain its login and permission helpers in context.

The final handler returns status 500 and a generic message. It logs a class, filename and line, rather than submitted passwords, messages or database exception text. This trades detailed automatic logging for reduced exposure in a beginner project; reproduce problems locally to investigate further.

Session startup is also checked: if server session storage is unavailable, the project stops rather than pretending login or form tokens can persist. A configured PHP installation needs a writable, private session.save_path; inspect its server log if sessions do not persist.

This is a last boundary, not a reason to ignore defects. It cannot guarantee a clean response if output was already sent. Keep request processing before HTML and handle expected validation explicitly.

09 | DEPLOYED SETTINGS ARE A SEPARATE DECISION

Configure for the environment
Local development Deployed service
Inspect detailed diagnostics privately Keep browser error display off, including startup errors
Use fictional data Control who can read logs and stored personal data
Local HTTP practice cookie settings HTTPS and Secure cookies
Known local paths Host-confirmed document root, permissions and private configuration

Use the host’s PHP settings to keep display_errors and display_startup_errors off and log_errors on for deployment. A handler inside one page does not replace those settings. Keep logs outside public access; avoid unnecessary sensitive content and plan log retention.

The local projects are learning examples. Later deployment requires abuse controls, account recovery, session expiry and a reviewed configuration rather than merely uploading this folder.

10 | BUILD A FAILURE TEST RECORD

TRY IT YOURSELF

Stop the local database, request a database page, inspect the browser and server log, then restart the database and retry. Use a separate scratch file to test a missing semicolon.

Reveal guidance

Database failure should give a controlled failure response and a server-side diagnostic, with no false saved message or credentials. The syntax failure must be corrected in the source. Record the cause, expected response, actual response and recovery action separately.

Next, use the shared session bootstrap to understand login and access decisions.

KEEP LEARNING

Previous page 04.2 Login concepts and access control

Download complete Section 04 example files

Official references: PHP error configuration · Password hashing · Password verification · Session identifier rotation