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
| 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
| 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