Home › Web Development › PHP
WEB DEVELOPMENT / PHP
Learn PHP from the fundamentals to server-side programming, forms, databases, APIs, object-oriented programming, WordPress development, security and modern backend development.
Every concept comes with a short, copyable example using secure patterns — prepared statements, hashed passwords and validated input throughout.
THE BASICS
PHP is a general-purpose scripting language used mainly for server-side web development, though it also runs from the command line for scripts, automation and tooling. It’s a full programming language with variables, functions, control flow, arrays, objects, classes and exceptions.
The defining characteristic is where it runs. PHP executes on the server, before anything reaches the browser. A visitor never sees your PHP source — only what it produced. That’s why PHP can safely hold database credentials and business logic, and why it’s suited to work the browser shouldn’t be trusted with.
In practice PHP generates HTML pages, returns JSON for APIs, processes form submissions, queries databases, manages sessions and logins, handles files and uploads, talks to other services and enforces authorisation rules. It’s also the foundation of WordPress and many other content management systems.
Describing PHP as “HTML scripting” sells it short. Generating markup is one thing it does; modern PHP applications are structured programs with routing, services, dependency management and automated tests.
WHERE PHP FITS
Structure and meaning. HTML isn't a programming language — it has no logic. PHP frequently generates it. See our HTML guide.
Presentation and layout, applied in the browser to whatever markup arrives. PHP never styles anything. See our CSS guide.
Runs in the browser for interaction, and outside it through runtimes such as Node.js. See our JavaScript guide.
Runs on the server: application logic, data access and response generation before the page is sent.
The honest comparison is about where each runs, not which is better. PHP executes on the server, so it can reach databases, files and secrets, and it produces the response. JavaScript traditionally runs in the browser, updating the interface and calling APIs — though Node.js runs it server-side too, so the boundary is about architecture rather than the languages themselves.
They’re complements, not rivals. A typical stack has PHP handling data, validation and authorisation server-side, and JavaScript improving the interface and fetching updates without a page reload. Neither is universally better; they solve different halves of the problem.
<?php $name = "Alex"; $roles = ["Developer", "Designer"]; ?> <h1>Hello, <?= htmlspecialchars($name) ?></h1> <ul> <?php foreach ($roles as $role): ?> <li><?= htmlspecialchars($role) ?></li> <?php endforeach; ?> </ul>
# Check the installed version php -v # Run a script php script.php # Built-in development server (not for production) php -S localhost:8000 -t public # Composer and framework commands run through PHP too composer install php artisan migrate
HOW IT RUNS
PHP source is just text until a PHP runtime executes it. That runtime parses the code, compiles it to an internal bytecode and runs it, then returns the output.
A web server such as Apache or Nginx handles the HTTP side: accepting connections, serving static files and deciding what needs PHP. It’s worth keeping the roles distinct — the web server doesn’t execute PHP, and PHP doesn’t handle HTTP connections.
PHP-FPM (FastCGI Process Manager) sits between them, managing a pool of PHP worker processes that the web server passes requests to. It isn’t “the PHP server” — it’s a process manager that keeps workers ready so a new process doesn’t have to start per request.
PHP also runs from the command line, with no web server involved. That’s how Composer, framework commands, maintenance scripts, cron jobs and build tooling run. The CLI uses the same language but a different configuration, so behaviour can differ from the web context.
SYNTAX
PHP code lives inside <?php ... ?> tags. In a file that is pure PHP, the closing tag is usually omitted to avoid accidental whitespace in the output. Statements end with semicolons, and echo sends output to the response.
Comments use // or # for one line and /* */ for several. Explain decisions rather than restating syntax, and never store credentials in them.
Variables begin with $ followed by a letter or underscore, then letters, digits or underscores. They’re case-sensitive — $name and $Name are different variables, a classic source of quiet bugs. PHP is dynamically typed, so a variable can hold any type and be reassigned to another.
Constants hold values that shouldn’t change. const is defined at compile time; define() at runtime, which allows conditional definitions. Configuration values, table prefixes and feature names are typical uses.
<?php
// Single-line comment
# Also a single-line comment
/*
Multi-line comment
*/
$name = "Alex"; // string
$age = 30; // integer
$price = 1499.50; // float
$active = true; // boolean
$missing = null; // null
echo $name;
echo "Name: $name, age: $age";
// Constants
const SITE_NAME = "WebDesigningPune";
define("MAX_UPLOAD_MB", 8);
echo SITE_NAME;<?php
$name = "Alex";
// Double quotes interpolate, single quotes do not
echo "Hello $name"; // Hello Alex
echo 'Hello $name'; // Hello $name
echo 'Hello ' . $name; // concatenation
// Common string functions
strlen($name); // 4
strtoupper($name); // ALEX
strtolower($name); // alex
trim(" spaced "); // "spaced"
str_replace("-", "/", $date);
strpos("webdesign", "design"); // 3 (or false)
substr("webdesign", 0, 3); // "web"
// Numbers
intdiv(7, 2); // 3
7 % 2; // 1
2 ** 10; // 1024
round(0.1 + 0.2, 2); // 0.3
// Booleans and null
$isActive = true;
$deleted = null;
is_null($deleted); // trueDATA TYPES
PHP has four scalar types — string, int, float and bool — plus array, object, null, callable and iterable. The resource type still appears for some older extension handles, though modern extensions increasingly return objects instead.
Strings behave differently depending on quotes: double quotes interpolate variables and interpret escape sequences, single quotes take the text literally and are marginally faster. Concatenation uses the . operator. Useful functions include strlen(), strtolower(), strtoupper(), trim(), str_replace(), strpos() and substr(). Note that strpos() returns false when not found and 0 for a match at the start, so compare with !== false rather than a truthiness check.
Numbers split into integers and floats. Floats are binary floating-point, so 0.1 + 0.2 doesn’t land exactly on 0.3 — for money, work in the smallest unit or use a decimal library rather than comparing floats for equality.
Booleans are true and false. NULL represents the absence of a value — an unset variable, a missing database column, a function with nothing to return.
ARRAYS
A PHP array is an ordered map. That single structure covers what other languages split into lists, dictionaries, stacks and queues — which is why it’s everywhere in PHP code. It is not equivalent to a JavaScript array, and keys can be integers or strings.
Indexed arrays use automatic numeric keys from zero. Associative arrays use named string keys and are the standard way to represent a record, a configuration set or a decoded JSON object. Multidimensional arrays nest arrays inside arrays, which is exactly what a database result set or an API response usually looks like.
The function library is large. count() counts elements, in_array() checks membership, array_keys() and array_values() extract each half, array_merge() combines, and array_map(), array_filter() and array_reduce() transform, select and fold. For sorting, sort() and rsort() reindex, while usort() takes your own comparison function — note that most sort functions modify the array in place rather than returning a new one.
<?php
// Indexed
$colors = ["red", "blue", "green"];
echo $colors[0]; // red
// Associative
$user = [
"name" => "Alex",
"age" => 30,
];
echo $user["name"]; // Alex
// Multidimensional
$users = [
["name" => "Alex", "role" => "Developer"],
["name" => "Sam", "role" => "Designer"],
];
echo $users[1]["role"]; // Designer
// Common array functions
count($colors); // 3
in_array("blue", $colors, true); // true
array_keys($user); // ["name", "age"]
$upper = array_map("strtoupper", $colors);
$devs = array_filter($users, fn($u) => $u["role"] === "Developer");
$total = array_reduce([10, 20, 30], fn($c, $n) => $c + $n, 0);
usort($users, fn($a, $b) => $a["name"] <=> $b["name"]);<?php
// Comparison
0 == "0"; // true — loose, converts types
0 === "0"; // false — strict, compares type too
"abc" == 0; // false in modern PHP
// Null coalescing provides a fallback
$name = $_GET["name"] ?? "Guest";
$city = $user["address"]["city"] ?? null;
// Spaceship operator returns -1, 0 or 1
1 <=> 2; // -1
if ($score >= 90) {
$grade = "A";
} elseif ($score >= 80) {
$grade = "B";
} else {
$grade = "C";
}
// match: strict comparison, returns a value
$label = match ($status) {
200, 201 => "Success",
404 => "Not Found",
default => "Other",
};
for ($i = 0; $i < 3; $i++) { }
while ($rows = $stmt->fetch()) { }
foreach ($colors as $color) {
if ($color === "red") continue;
if ($color === "green") break;
echo $color;
}
foreach ($user as $key => $value) {
echo "$key: $value";
}CONTROL FLOW
PHP has the usual arithmetic operators plus % and **, compound assignment operators, comparison operators, logical &&, || and !, the ternary, and the spaceship operator <=> for comparisons in sort callbacks.
== versus ===: loose comparison converts types before comparing, strict comparison also requires the types to match. Default to ===, since loose comparison has historically produced surprising results. That doesn’t make == invalid — it’s occasionally what you want — but strict comparison is the safer habit, and string-to-number comparison rules changed in PHP 8 to be less surprising.
The null coalescing operator ?? returns the right side when the left is null or undefined, without an undefined-index warning. It’s the standard way to read optional request data, and ??= assigns only if currently null.
Conditionals use if, elseif, else and switch. The match expression, added in PHP 8.0, is often better: it returns a value, compares strictly, requires no break, and throws if nothing matches and no default is given.
Loops are for, while, do...while and foreach — the last being the one you’ll use most, iterating arrays either by value or as key-value pairs. break exits a loop; continue skips to the next iteration.
FUNCTIONS
Functions take parameters, can declare defaults, and return values. Type declarations on parameters and return values are one of the most valuable habits in modern PHP: they document intent, catch mistakes early, and in files declaring strict_types=1 they’re enforced rather than coerced.
Union types (PHP 8.0) allow more than one accepted type, written int|string. Nullable types use a ? prefix, so ?array means an array or null — useful for lookups that may find nothing.
Anonymous functions are values you can pass around, and they capture outer variables explicitly with use — PHP doesn’t capture automatically the way JavaScript does. An anonymous function that captures variables this way is a closure. Arrow functions (PHP 7.4), written fn() =>, take a single expression and capture outer variables automatically by value, which makes them neat for short callbacks.
Scope in PHP is stricter than many expect: a function cannot see variables from outside it unless they’re passed in, captured, or pulled in with global. Reaching for global creates hidden dependencies and makes code hard to test — passing values as parameters is almost always better. Static variables persist between calls to the same function.
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
// Union type (PHP 8.0+)
function formatId(int|string $id): string
{
return (string) $id;
}
// Nullable parameter and return
function findUser(?int $id): ?array
{
return $id === null ? null : ["id" => $id];
}
// Anonymous function with explicit capture
$tax = 0.18;
$withTax = function (float $price) use ($tax): float {
return $price + ($price * $tax);
};
// Arrow function captures automatically
$double = fn(int $n): int => $n * 2;
// Static variable persists between calls
function counter(): int
{
static $count = 0;
return ++$count;
}STRUCTURE
include and require both pull in another PHP file. The difference is failure behaviour: a missing include emits a warning and execution continues, while a missing require is a fatal error. Use require for anything the application can’t run without. The _once variants prevent the same file being loaded twice, which matters for files that define functions or classes. In modern applications, Composer’s autoloader usually replaces manual includes for classes.
For file organisation, the useful principle is separating responsibilities: configuration apart from logic, database access apart from presentation, reusable services apart from page-specific code. A common arrangement puts only the entry point in a public web directory and everything else outside it, so source files can’t be requested directly. Structures vary by project and framework — treat this as a principle, not a mandated layout.
Superglobals are arrays available in every scope. $_GET holds query parameters, $_POST form data, $_SERVER request and environment details, $_SESSION session data, $_COOKIE cookies and $_FILES uploads. $_REQUEST merges several sources, which makes the origin of a value ambiguous — prefer the specific array so you know what you’re reading.
The essential rule: every superglobal except $_SESSION contains data the user controls. Even parts of $_SERVER derive from request headers. Nothing from them should reach a database, a file path, an HTML page or a shell command without validation and context-appropriate escaping.
<?php require_once __DIR__ . "/../config/database.php"; include __DIR__ . "/partials/header.php"; // Always treat request data as untrusted $search = trim($_GET["q"] ?? ""); $email = trim($_POST["email"] ?? ""); $method = $_SERVER["REQUEST_METHOD"] ?? "GET"; // Prefer the specific superglobal over $_REQUEST // so the source of the value is unambiguous
FORMS
Form handling is where most PHP applications meet untrusted input, so it’s worth doing carefully every time.
Validation asks whether the data is acceptable; sanitisation modifies it; escaping makes it safe for a particular output context. They’re separate jobs, and “sanitise everything” is not a security strategy — it often mangles legitimate input while leaving real problems in place. Validate strictly, reject what fails, and escape at the point of output.
filter_var() covers common checks with filters such as FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL and FILTER_VALIDATE_INT. Collect all the errors rather than stopping at the first, so the person can fix everything in one pass.
Output escaping is context-sensitive. htmlspecialchars() with ENT_QUOTES and a charset handles HTML, but a value going into a URL, a JavaScript block or an SQL query needs a different treatment entirely — URL encoding, JSON encoding and prepared statements respectively.
Client-side validation improves the experience and nothing more. It can be bypassed trivially, so the server must validate independently, always.
<?php
declare(strict_types=1);
session_start();
$errors = [];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// CSRF check before anything else
$token = $_POST["csrf_token"] ?? "";
if (!hash_equals($_SESSION["csrf_token"] ?? "", $token)) {
http_response_code(400);
exit("Invalid request.");
}
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");
$age = filter_input(INPUT_POST, "age", FILTER_VALIDATE_INT);
if ($name === "") {
$errors["name"] = "Enter your name.";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "Enter a valid email address.";
}
if ($age === false || $age < 18) {
$errors["age"] = "Enter an age of 18 or over.";
}
if (!$errors) {
// Process, then redirect to avoid resubmission
header("Location: /thank-you/", true, 303);
exit;
}
}
// Generate a CSRF token for the form
$_SESSION["csrf_token"] ??= bin2hex(random_bytes(32));
?>
<!-- Escape every value written back into HTML -->
<input name="name" value="<?= htmlspecialchars($name ?? "", ENT_QUOTES, "UTF-8") ?>">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($_SESSION["csrf_token"], ENT_QUOTES, "UTF-8") ?>">STATE
HTTP is stateless, so PHP needs a mechanism to recognise the same visitor across requests. Sessions do this by keeping data on the server and giving the browser only an identifier, usually in a cookie. session_start() begins or resumes a session; $_SESSION reads and writes the data.
Sessions suit login state, multi-step forms, flash messages and temporary per-user data. Two habits matter: call session_regenerate_id(true) immediately after a successful login, which limits session fixation risks, and destroy the session properly on logout rather than only unsetting a variable. Keep the contents minimal — session storage is server-side but still shouldn’t hold more sensitive data than the task requires.
Cookies are set with setcookie() and stored by the browser, returning with each matching request. A cookie without attributes is not secure by default. HttpOnly prevents JavaScript reading it, which protects a session identifier from cross-site scripting. Secure restricts it to HTTPS. SameSite controls whether it’s sent on cross-site requests, which relates directly to CSRF. Set these deliberately; the defaults are not what you want for anything sensitive.
<?php
session_start();
// After verifying credentials
session_regenerate_id(true);
$_SESSION["user_id"] = $user["id"];
// Reading session state
if (empty($_SESSION["user_id"])) {
header("Location: /login/", true, 302);
exit;
}
// Logging out properly
$_SESSION = [];
session_destroy();
// A cookie with sensible attributes
setcookie("preferred_theme", "dark", [
"expires" => time() + 60 * 60 * 24 * 30,
"path" => "/",
"secure" => true, // HTTPS only
"httponly" => true, // not readable by JavaScript
"samesite" => "Lax",
]);<?php
// Reading and writing
$contents = file_get_contents($path);
file_put_contents($path, $data, LOCK_EX);
// Streaming a large file line by line
$handle = fopen($path, "r");
if ($handle !== false) {
while (($line = fgets($handle)) !== false) {
// process one line at a time
}
fclose($handle);
}
// Handling an upload defensively
$file = $_FILES["document"] ?? null;
if (!$file || $file["error"] !== UPLOAD_ERR_OK) {
exit("Upload failed.");
}
if ($file["size"] > 2 * 1024 * 1024) {
exit("File is too large.");
}
// Check the real type, not the submitted one
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file["tmp_name"]);
$allowed = ["image/jpeg" => "jpg", "image/png" => "png"];
if (!isset($allowed[$mime])) {
exit("File type not allowed.");
}
// Generate the filename; never trust the submitted one
$name = bin2hex(random_bytes(16)) . "." . $allowed[$mime];
$destination = STORAGE_PATH . "/uploads/" . $name;
move_uploaded_file($file["tmp_name"], $destination);FILES
For simple reads and writes, file_get_contents() and file_put_contents() are enough. For large files, the fopen(), fgets(), fwrite(), fclose() family lets you stream rather than loading everything into memory. Always check that operations succeeded and be deliberate about file permissions.
Uploads deserve real care, because an upload endpoint accepts arbitrary bytes from anyone. Several checks work together: confirm the upload succeeded via the error key, enforce a size limit, and determine the type by inspecting the file itself with finfo rather than trusting the submitted MIME type or extension, both of which the client controls.
Never use the submitted filename. Generate your own from a random value and an extension you derived from the verified type. Store uploads outside the web root, or in a directory configured not to execute scripts, and serve them through PHP with an authorisation check where appropriate. Use move_uploaded_file(), which verifies the file genuinely came from an upload.
Applying only one of these checks is the usual mistake — they’re layers, and the combination is what makes an upload endpoint defensible.
DATABASES
Most PHP applications store data in a relational database — MySQL, MariaDB, PostgreSQL or SQLite are all commonly used, and the right one depends on the project rather than any ranking.
PDO (PHP Data Objects) provides a consistent interface across those databases, so the same code patterns work whichever you use. Configure it deliberately: set the error mode to throw exceptions so failures surface instead of passing silently, set the default fetch mode, and disable emulated prepares so the database handles preparation itself.
Prepared statements are the core practice. The SQL is sent with placeholders, then the values are sent separately, so the database never treats user input as part of the query structure. That’s what prevents SQL injection — not escaping, not filtering, not clever string checks.
The unsafe pattern is concatenating input into SQL. It’s shown below only as the thing to recognise and avoid; with a prepared statement the same input is just a value, whatever it contains. Placeholders can’t be used for table or column names, so if those ever vary, check them against an allowed list you define.
<?php
declare(strict_types=1);
$dsn = "mysql:host=localhost;dbname=DB_NAME;charset=utf8mb4";
$pdo = new PDO($dsn, "DB_USER", "DB_PASSWORD", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// NEVER do this — user input inside the SQL string
// $sql = "SELECT * FROM users WHERE email = '$email'";
// Prepared statement: values stay separate from the query
$stmt = $pdo->prepare(
"SELECT id, name, email FROM users WHERE email = :email"
);
$stmt->execute(["email" => $email]);
$user = $stmt->fetch(); // one row, or false
// $users = $stmt->fetchAll(); // all rows<?php
// CREATE
$stmt = $pdo->prepare(
"INSERT INTO products (name, price) VALUES (:name, :price)"
);
$stmt->execute(["name" => $name, "price" => $price]);
$newId = $pdo->lastInsertId();
// READ with pagination
$stmt = $pdo->prepare(
"SELECT id, name, price FROM products ORDER BY id DESC LIMIT :limit"
);
$stmt->bindValue("limit", 20, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
// UPDATE
$pdo->prepare("UPDATE products SET price = :price WHERE id = :id")
->execute(["price" => $price, "id" => $id]);
// DELETE
$pdo->prepare("DELETE FROM products WHERE id = :id")
->execute(["id" => $id]);
// Transaction: all steps succeed, or none do
try {
$pdo->beginTransaction();
$pdo->prepare("INSERT INTO orders (user_id) VALUES (:user)")
->execute(["user" => $userId]);
$pdo->prepare("UPDATE stock SET qty = qty - 1 WHERE id = :id")
->execute(["id" => $productId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
error_log($e->getMessage());
throw $e;
}CRUD — create, read, update, delete — covers most database work, mapping to INSERT, SELECT, UPDATE and DELETE. Real applications add authorisation checks before every one of them: a prepared statement stops injection, but it won’t stop a logged-in user modifying someone else’s record if you never checked ownership.
Transactions group statements so they either all succeed or all roll back. Any operation touching several tables — creating an order while decrementing stock, transferring a value between records — needs one, or a failure halfway leaves the data inconsistent.
<?php
declare(strict_types=1);
namespace App\Models;
interface Loggable
{
public function log(string $message): void;
}
trait TimestampTrait
{
public function touch(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
}
abstract class Model
{
abstract public function table(): string;
}
class User extends Model implements Loggable
{
use TimestampTrait;
private array $roles = [];
// Constructor property promotion (PHP 8.0+)
public function __construct(
public readonly string $name,
protected string $email,
) {}
public function table(): string
{
return "users";
}
public function log(string $message): void
{
error_log("[user] {$message}");
}
public function addRole(string $role): void
{
$this->roles[] = $role;
}
}
$user = new User("Alex", "alex@example.com");
echo $user->name;OBJECT-ORIENTED PHP
Classes define the shape and behaviour of objects; an object is an instance created with new. The constructor __construct() runs on creation and typically receives the object’s dependencies.
Encapsulation is controlled through visibility: public is accessible anywhere, protected within the class and its subclasses, private within the defining class only. Keeping internal state private means you can change how a class works without breaking everything that uses it.
Inheritance with extends shares behaviour from a parent class, and an abstract class defines a partial base that can’t be instantiated. Interfaces declare a contract of methods without implementations, which is what enables polymorphism — any class satisfying the interface can be used interchangeably. In practice, depending on interfaces rather than concrete classes is what makes code testable.
Traits provide horizontal reuse for behaviour shared across unrelated classes, filling a gap left by PHP’s single inheritance. Static members belong to the class rather than an instance — useful for factories and utilities, though heavy static use makes testing harder.
Namespaces prevent naming collisions and organise code, and they’re what PSR-4 autoloading maps to directories.
DEPENDENCIES
Composer is the dependency manager for PHP. It is not a framework — it installs libraries, resolves version constraints and generates an autoloader. Packagist is the main public repository it pulls from; evaluate any package you add for maintenance, licence and necessity rather than installing on reputation alone.
Two files matter. composer.json declares what you want and the version ranges you accept. composer.lock records exactly what was installed, so every environment gets identical versions — commit it for applications. The vendor/ directory holds the installed code and is not committed.
Autoloading removes manual require statements for classes. Under the PSR-4 convention, a namespace prefix maps to a directory, so App\Services\Mailer is found at src/Services/Mailer.php. Including Composer’s autoloader once at the entry point is then enough for the whole application.
// composer.json
{
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
// Terminal
// composer install
// composer require vendor/package
// composer dump-autoload
<?php
// public/index.php — one include for everything
require __DIR__ . "/../vendor/autoload.php";
use App\Services\Mailer; // resolved to src/Services/Mailer.php
$mailer = new Mailer();ERRORS
PHP signals problems through exceptions and errors. Since PHP 7 both implement the Throwable interface, so catching Throwable catches everything, while catching Exception catches only the exception branch. try runs risky code, catch handles specific types, finally always runs, and throw raises your own.
Catching an exception and continuing silently is worse than not catching it — failures become invisible. Either handle it meaningfully, or let it propagate to a handler that logs it and returns a sensible response.
Environment matters enormously here. In development, displaying errors speeds things up. In production, detailed errors must never reach the browser: stack traces expose file paths, database structure and sometimes credentials. Log the detail, show the user a generic message.
Logging through error_log() or a logging library gives you the detail after the fact. Log enough context to diagnose — what failed, where, which request — but never passwords, tokens, card details or personal data. For debugging tools, PHP error logs and web server logs come first, with Xdebug and IDE integration for step debugging, browser developer tools for the request side, and database query logs where relevant. None of these are mandatory for every project.
Find the exact request that triggers it.
Message, file and line usually name the cause.
PHP error log first, then web server and application logs.
Method, parameters, headers and session state.
Check the values actually arriving, not the assumed ones.
Run the query directly; test the API call separately.
Distinguish the symptom from what produced it.
Address the cause, and consider similar code elsewhere.
Verify success and failure paths before shipping.
<?php
declare(strict_types=1);
final class PaymentFailedException extends RuntimeException {}
try {
if (!$gateway->charge($amount)) {
throw new PaymentFailedException("Gateway declined the charge");
}
} catch (PaymentFailedException $e) {
error_log("[payment] " . $e->getMessage());
$userMessage = "We could not process that payment.";
} catch (Throwable $e) {
error_log($e->getMessage());
$userMessage = "Something went wrong. Please try again.";
} finally {
$gateway->close();
}
// Development vs production
if (APP_ENV === "production") {
ini_set("display_errors", "0"); // never show details publicly
ini_set("log_errors", "1");
} else {
ini_set("display_errors", "1");
}
// Log context, never secrets
error_log(sprintf("[order] failed for user %d", $userId));<?php
declare(strict_types=1);
// Encoding and decoding
$data = ["name" => "Alex", "role" => "Developer"];
$json = json_encode($data, JSON_THROW_ON_ERROR);
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
// A minimal JSON endpoint
header("Content-Type: application/json; charset=utf-8");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo json_encode(["error" => "Method not allowed"]);
exit;
}
$input = json_decode(file_get_contents("php://input"), true);
if (!is_array($input) || empty($input["email"])) {
http_response_code(422);
echo json_encode(["error" => "email is required"]);
exit;
}
http_response_code(201);
echo json_encode(["success" => true, "id" => $newId]);APIS
JSON is how PHP usually exchanges data with browsers and other services. json_encode() converts PHP values to JSON text and json_decode() converts back, with true as the second argument giving associative arrays instead of objects. Passing the JSON_THROW_ON_ERROR flag turns silent failures into exceptions, which is worth doing by default.
A REST API in PHP maps HTTP methods to actions: GET retrieves, POST creates, PUT replaces, PATCH partially updates and DELETE removes. Read a JSON request body from php://input rather than $_POST, which only populates for form encodings.
Status codes carry meaning that clients rely on. Returning 200 with an error message inside the body is a common mistake — it tells every automated consumer that everything went fine. The list beside this is a common subset, not exhaustive.
API security follows the same principles as the rest of the page: authenticate the caller, authorise the specific action, validate every input, keep secrets out of source control, serve over HTTPS, apply rate limiting where abuse is plausible, and never return internal error detail. More in our website APIs guide.
SECURITY
Authentication answers “who are you?” and authorisation answers “what are you allowed to do?” Conflating them is a frequent and serious bug: a logged-in user is not automatically entitled to edit record 42. Check ownership or role on every protected action, not only at login.
Passwords must never be stored in plaintext, and never hashed with MD5 or SHA-1, which are unsuitable for passwords. password_hash() with PASSWORD_DEFAULT handles salting and algorithm choice for you, and password_verify() checks a submitted password against the stored hash. password_needs_rehash() lets you upgrade hashes as defaults improve. Store the result in a column long enough for future algorithms.
CSRF (cross-site request forgery) tricks a logged-in user’s browser into submitting a request they didn’t intend. The defence is a token tied to the session, included in every state-changing form and compared with hash_equals(), alongside a sensible SameSite cookie setting. Requests that change data should never be GET.
XSS (cross-site scripting) happens when untrusted input is written into a page and interpreted as markup. Escape on output with htmlspecialchars() using ENT_QUOTES and an explicit charset, and choose the escaping that matches the context — HTML, attribute, URL and JavaScript contexts each need different treatment. A Content Security Policy adds a second layer.
<?php
declare(strict_types=1);
// Registration — hash, never store plaintext
$hash = password_hash($password, PASSWORD_DEFAULT);
$pdo->prepare("INSERT INTO users (email, password_hash) VALUES (:e, :h)")
->execute(["e" => $email, "h" => $hash]);
// Login — verify against the stored hash
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = :e");
$stmt->execute(["e" => $email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user["password_hash"])) {
session_regenerate_id(true);
$_SESSION["user_id"] = $user["id"];
} else {
// Same message either way — do not reveal which was wrong
$error = "Invalid email or password.";
}
// Authorisation: authenticated is not the same as permitted
function canEditPost(array $user, array $post): bool
{
return $user["role"] === "admin"
|| $user["id"] === $post["author_id"];
}
if (!canEditPost($currentUser, $post)) {
http_response_code(403);
exit("Not permitted.");
}<?php
// Avoid queries inside loops (the N+1 problem)
// Instead of one query per user, fetch related rows together
$stmt = $pdo->prepare(
"SELECT o.id, o.total, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.created_at > :since
LIMIT 100"
);
$stmt->execute(["since" => $since]);
// Select only the columns you need, not SELECT *
// Stream large result sets instead of loading everything
while ($row = $stmt->fetch()) {
processRow($row);
}
// Generators keep memory flat for large datasets
function readLines(string $path): Generator
{
$handle = fopen($path, "r");
while (($line = fgets($handle)) !== false) {
yield $line;
}
fclose($handle);
}
foreach (readLines($bigFile) as $line) {
// one line in memory at a time
}PERFORMANCE
In most PHP applications the bottleneck is the database, not the language. Before optimising code, measure — profiling tells you where time actually goes, and assumptions are usually wrong.
OPcache stores compiled PHP bytecode in shared memory so scripts aren’t recompiled on every request. It’s a standard part of a production setup. The benefit varies by application and configuration, so treat any specific figure you read with scepticism.
Database performance is where the wins usually are: add indexes for columns used in WHERE and JOIN clauses, select only the columns you need, paginate large result sets, and watch for the N+1 problem where a loop fires one query per row. Connection handling and query design matter more than micro-optimising PHP syntax.
Caching operates at several levels — HTTP caching at the edge, full page caching, object caching for expensive computed results, and query result caching. Which combination fits depends on how dynamic and personalised the content is; caching a logged-in dashboard needs very different handling from a public article.
Memory is managed automatically through reference counting and garbage collection. What you control is how much you load at once: fetching a hundred thousand rows into an array will hit the memory limit, while streaming or using a Generator keeps usage flat. Keep PHP itself on a supported version — newer releases bring performance and security improvements. See website speed.
WORDPRESS
WordPress is built primarily in PHP, which is why PHP knowledge is what separates configuring WordPress from genuinely developing with it.
Themes use PHP template files selected by the template hierarchy, with the Loop iterating the posts WordPress has already queried. Plugins add functionality without touching core or theme files, which is why they survive updates.
Hooks are the central mechanism, and the distinction matters: an action runs code at a specific point and returns nothing, while a filter receives a value, modifies it and must return it. Forgetting to return from a filter is a classic bug that blanks content.
For database work, prefer WordPress APIs such as WP_Query and the post and meta functions over raw SQL — they handle escaping, caching and compatibility. When you genuinely need custom SQL, use $wpdb->prepare(). Also use WordPress’s own escaping and nonce functions rather than reinventing them.
A modern WordPress site isn’t PHP alone: the block editor is JavaScript, the REST API serves JSON, and front-end interactivity is JavaScript. PHP handles the server side of that picture. In Elementor projects, PHP belongs in a child theme’s functions file or a custom plugin — for custom queries, dynamic content, hooks and API integrations. Never edit Elementor’s core files; updates will overwrite the changes. Related: our CMS guides and WordPress guides.
<?php
// Action: do something at a defined point
add_action("init", function (): void {
register_post_type("project", [
"public" => true,
"label" => "Projects",
"supports" => ["title", "editor", "thumbnail"],
]);
});
// Filter: receive a value, change it, RETURN it
add_filter("excerpt_length", function (int $length): int {
return 30;
});
// Enqueue scripts properly rather than hard-coding tags
add_action("wp_enqueue_scripts", function (): void {
wp_enqueue_style(
"child-style",
get_stylesheet_uri(),
[],
wp_get_theme()->get("Version")
);
});
// Prefer WordPress APIs over raw SQL
$query = new WP_Query([
"post_type" => "project",
"posts_per_page" => 6,
]);
// Escape on output, as always
echo esc_html(get_the_title());<?php
declare(strict_types=1);
// --- AJAX endpoint called by fetch() ---
header("Content-Type: application/json; charset=utf-8");
session_start();
if (empty($_SESSION["user_id"])) {
http_response_code(401);
echo json_encode(["error" => "Not signed in"]);
exit;
}
$input = json_decode(file_get_contents("php://input"), true);
$query = trim((string) ($input["q"] ?? ""));
$stmt = $pdo->prepare(
"SELECT id, name FROM products WHERE name LIKE :q LIMIT 10"
);
$stmt->execute(["q" => "%{$query}%"]);
echo json_encode(["results" => $stmt->fetchAll()]);
// --- Webhook receiver: verify before trusting ---
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $payload, WEBHOOK_SECRET);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
// Process the event, then respond quickly
http_response_code(200);PHP + JAVASCRIPT
The common modern pattern has PHP serving data rather than whole pages. JavaScript calls a PHP endpoint with fetch(), PHP validates and queries, and returns JSON that JavaScript renders — no full page reload.
Everything security-related still belongs on the PHP side. The endpoint must authenticate the caller, check authorisation, validate input and use prepared statements, exactly as a form handler would. An AJAX endpoint is a public URL that anyone can call directly with any payload; the fact that your JavaScript calls it politely is irrelevant.
PHP also consumes APIs — calling external services, handling their JSON, managing credentials and timeouts server-side where they stay private. That’s often the right place for third-party integrations, since browser code can’t hold a secret.
Webhooks reverse the direction: an external service sends your endpoint an HTTP request when something happens. Verify the request before acting on it, typically by computing an HMAC signature over the raw body with a shared secret and comparing using hash_equals() to avoid timing leaks. Respond quickly and process heavier work afterwards, since senders usually retry on a timeout.
FRAMEWORKS
PHP is the language; Laravel is a framework written in PHP. That distinction matters — Laravel isn’t a different language or a replacement for PHP knowledge, it’s a structured set of tools built on it. Everything on this page still applies inside a Laravel application.
Laravel provides routing, controllers, Eloquent for database access, Blade for templates, middleware for cross-cutting request handling, and the Artisan command line. Other PHP frameworks make different trade-offs; the concepts transfer. Our Laravel guide covers it properly.
MVC is the organising pattern most PHP frameworks follow loosely: models handle data and business rules, controllers receive requests and coordinate, views handle presentation. Frameworks interpret it differently, and larger applications usually add a service layer so controllers stay thin. It’s a way of separating responsibilities, not a specification.
The underlying architecture is consistent whether you use a framework or not: a request arrives, routing decides what handles it, application logic coordinates services, those services talk to databases and external APIs, and a response is returned. A framework gives you that structure ready-made rather than assembled by hand.
MODERN PHP
These features arrived in different PHP releases, so check what your hosting actually runs before relying on them. Version numbers are given where they’re certain.
Parameter, return and property types, with strict_types for enforcement.
Accept more than one type, written int|string. PHP 8.0+.
?array means the type or null.
Returns a value, compares strictly, no fall-through. PHP 8.0+.
Declare and assign properties in the constructor signature. PHP 8.0+.
A fixed set of named values with optional backing types. PHP 8.1+.
Pass arguments by parameter name, skipping optional ones. PHP 8.0+.
Assigned once, then immutable. PHP 8.1+, with readonly classes in 8.2.
Structured metadata on classes and methods, readable via reflection. PHP 8.0+.
Low-level primitive for cooperative multitasking, mostly used by libraries. PHP 8.1+.
<?php
declare(strict_types=1);
// Backed enum (PHP 8.1+)
enum Status: string
{
case Draft = "draft";
case Published = "published";
public function label(): string
{
return match ($this) {
Status::Draft => "Draft",
Status::Published => "Published",
};
}
}
$status = Status::from("draft");
echo $status->label();
// Readonly + promotion (PHP 8.1+)
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency = "INR",
) {}
}
// Named arguments (PHP 8.0+)
$price = new Money(amount: 1499, currency: "INR");
// Attribute (PHP 8.0+)
#[Deprecated]
class LegacyService {}Tests are how you change code confidently. Unit tests check one class or function in isolation, integration tests check components working together such as a repository against a real database, and feature or API tests exercise a whole request and its response.
PHPUnit is the most widely used PHP testing framework, installed through Composer. Writing tests for a five-line script is overkill; writing them for authorisation rules, pricing logic, validation and API contracts pays for itself the first time you refactor.
Test the failure paths as deliberately as the success ones. Invalid input, missing records, permission denials and failed external calls are where production bugs live, and they’re the cases developers most often skip.
REFERENCE
A categorised quick reference for the syntax and functions covered above. For authoritative details on any function, use the official PHP documentation at php.net rather than a summary like this one.
<?phpecho$variableconst///* */stringintfloatboolarraynullifelseifelseswitchmatch???:forwhiledo...whileforeachbreakcontinuefunctionreturnfn() =>use ()staticcount()in_array()array_map()array_filter()array_reduce()usort()strlen()trim()str_replace()substr()strpos()$_GET$_POST$_FILESfilter_var()htmlspecialchars()session_start()$_SESSIONsetcookie()PDOprepare()execute()fetch()beginTransaction()classextendsimplementstraitnamespacepublicjson_encode()json_decode()trycatchfinallythrowINTERVIEW PREP
Concise, technically accurate answers to the PHP questions that come up most often in interviews and technical assessments.
CHECKLISTS
Three review passes over the same application. Practical working lists rather than formal audits — the security list is a baseline, not a complete threat model.
SECURITY [ ] Validate all input server-side [ ] Escape output for its context [ ] Use prepared statements for every query [ ] Hash passwords with password_hash() [ ] Protect state-changing forms with CSRF tokens [ ] Set Secure, HttpOnly and SameSite on cookies [ ] Serve everything over HTTPS [ ] Restrict and verify file uploads [ ] Check authorisation on every protected action [ ] Protect and rate-limit API endpoints [ ] Keep PHP and dependencies updated [ ] Keep secrets out of source control [ ] Disable detailed errors in production [ ] Log security-relevant events [ ] Review third-party dependencies PERFORMANCE [ ] Run a supported PHP version [ ] Enable OPcache in production [ ] Optimise slow database queries [ ] Add indexes for WHERE and JOIN columns [ ] Cache expensive operations [ ] Avoid queries inside loops [ ] Avoid loading huge datasets at once [ ] Optimise external API calls [ ] Profile before optimising [ ] Review application architecture [ ] Test under realistic conditions CODE QUALITY [ ] Responsibilities are separated [ ] Functions and classes stay focused [ ] Types are declared [ ] Errors are handled and logged [ ] Secrets come from configuration [ ] Database access is centralised [ ] Input validation is consistent [ ] Output escaping is consistent [ ] Failure paths are tested [ ] Dependencies are pinned in composer.lock
PRACTICE
Never trust anything that arrived over the network.
Match the escaping to the output context.
The only reliable defence against SQL injection.
password_hash() and password_verify(), never plaintext or MD5.
Keep logic, data access and presentation apart.
Manage dependencies and autoloading properly.
Catch what you can act on; log the rest.
Detailed errors belong in logs, not in the browser.
Supported versions receive security fixes.
Success and failure, including permission denials.
Assuming form or URL data is safe.
Building queries from input instead of using placeholders.
Or MD5 and SHA-1, which are unsuitable for passwords.
Checking identity but never permission.
State-changing forms without a session-bound token.
Trusting the submitted filename or MIME type.
Stack traces revealing paths and structure.
Queries and business rules inside view files.
Thousands of lines doing everything.
Hidden dependencies that resist testing.
Empty catch blocks hiding real failures.
Queries scanning entire tables.
The N+1 problem multiplying database round trips.
Running releases that no longer get security fixes.
Credentials committed to the repository.
Rendering stored input straight into HTML.
No regeneration after login, no proper logout.
No way to know something broke until a user reports it.
LEARNING PATH
A workable order for learning PHP. Security appears after databases and authentication rather than at the end, because those are the sections where the habits need forming — retrofitting security onto a finished application is far harder.
These are projects to build yourself rather than templates to download. Each isolates a set of techniques, and each will raise the questions that only appear when real input arrives.
Beginner — POST handling, validation, escaping, CSRF.
Beginner — functions, conditions and input handling.
Beginner — arrays, sessions and form round-trips.
Intermediate — password hashing, validation, unique constraints.
Intermediate — PDO, prepared statements, pagination.
Intermediate — OOP, routing, templates and authorisation.
Intermediate — type verification, safe storage, permissions.
Advanced — JSON, status codes, authentication, validation.
Advanced — sessions, roles, password resets, CSRF.
Advanced — transactions, reporting queries, caching.
Advanced — hooks, WP APIs, settings, escaping.
NEXT TOPICS
Learn how HTML structures web content and application interfaces.
Learn how CSS controls layouts, styling and responsive design.
Learn browser-side programming, DOM manipulation and interactive interfaces.
Explore modern PHP framework development with routing, Eloquent and Blade.
Understand how applications communicate through HTTP and JSON APIs.
Learn component-based frontend development using JavaScript.
Understand content management systems including WordPress.
Learn where PHP applications run and how they are deployed.
FAQ
Short answers to the questions people ask most about PHP and backend development.
Learn how PHP handles server-side logic, forms, databases, APIs, authentication and dynamic web applications — and connect those skills with WordPress, JavaScript and modern PHP frameworks.