PHP Basics — Modern Server-Side Scripting

Status: Active | Last Updated: 2026-08-29 Category: Languages — PHP Prerequisites: Basic programming concepts (see kb/basics/) Tags: php, web, server, composer, scripting Estimated Time: 4-6 hours (Self-paced, includes lab time)

Summary

PHP powers a large portion of the web (WordPress, Drupal, many CMS and e-commerce platforms). Modern PHP (8.0+) has features — typed properties, union types, attributes, named arguments — that make it a modern, competitive language. This article covers PHP syntax, modern features, and when PHP makes sense.

What You'll Learn (Core Competencies)

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

Why PHP

PHP is easy to deploy: upload a .php file to a web server and it runs. It requires no build step, no separate runtime installation (the server has PHP built in or installed), and integrates naturally with HTML. Modern PHP has improved dramatically:

The Request/Response Execution Model

PHP is a shared-nothing, request-scoped language. Every HTTP request (or CLI invocation) starts with a fresh PHP process, executes the script, returns the response, and exits:

Stage What Happens Notes
Request arrives Web server (Nginx/Apache) routes to a .php file Or CLI invocation
Process starts PHP engine compiles the script to opcodes Or loads from opcache
Script runs Top-level code, then handles the request No state carries over from the previous request
Response sent Output is buffered, sent to the client Or written to stdout in CLI
Process exits All state is destroyed Memory is freed

Because each request is isolated, there is no concept of in-memory state between requests. Persistence is achieved through external systems: databases, caches (Redis, Memcached), session storage, files. This is the opposite of long-running servers like Node.js or Go, where one process serves many requests and shares state.

PHP 8: A Modern Language

PHP 8 (released 2020) was a major turning point. The language picked up features that put it on par with TypeScript and other modern typed languages:

For new projects in 2026, PHP 8.2+ is the baseline. Anything earlier is legacy.


2. Deep Dive & Implementation

Basic Syntax

<?php
$name = "fogserv";        // string
$count = 42;             // int
$ratio = 3.14;           // float
$enabled = true;         // bool
$items = ["a", "b"];    // array

function greet(string $name): string {
    return "Hello, $name";
}

echo greet($name);
?>

Variables start with $. Arrays work for both indexed and associative data (["key" => "value"]). Functions declare parameter and return types.

Variables and Types

<?php
declare(strict_types=1);  // enforce types strictly (recommended)

$integer = 42;        // int
$float = 3.14;        // float
$string = "hello";    // string
$bool = true;         // bool
$string = "world";    // variables are mutable
$null = null;         // null value

// Arrays
$indexed = ["apple", "banana", "cherry"];
$assoc = ["a" => 1, "b" => 2];
$mixed = [1, "hello", ["nested"]];

// Constants (constants are case-sensitive by default)
const VERSION = "1.0.0";
define("APP_MODE", "production");

// Type casting
$int_str = (int)"42";    // 42
$float_str = (float)"3.14"; // 3.14
$str_int = (string)42;    // "42"

// Type checking
if (is_int($integer) && is_string($string)) {
    echo "types are correct\n";
}

Strict types (declare(strict_types=1)) change how type coercion works. Without it, a function expecting int will coerce "42" to 42. With strict types, it throws a TypeError. Always use strict_types=1 in production.

Strings

$first = "Alice";
$last = 'Smith';

// Interpolation with double quotes
$full = "$first $last";        // "Alice Smith"
$full = $first . ' ' . $last;  // same with concatenation

// Multiline with heredoc
$text = <<<EOT
This is a multiline string.
Variables like $first are interpolated.
EOT;

$multiline = "line one\nline two\nline three";

// Useful methods
$len = strlen($full);        // length
$sub = substr("hello", 1, 3); // "ell"
$pos = strpos($text, "line"); // first occurrence
$replaced = str_replace("old", "new", $text);
$trimmed = trim("  text  ");  // "text"

Functions

// Basic function
function add(int $a, int $b): int {
    return $a + $b;
}

// Default parameters
function greet(string $name = "World"): string {
    return "Hello, $name";
}

// Variadic arguments
function sum(...$numbers): int {
    return array_sum($numbers);
}
echo sum(1, 2, 3, 4);  // 10

// Named arguments (PHP 8+)
echo greet(name: "Fogserv");

// Arrow functions (short closures)
$square = fn($n) => $n * $n;
$evens = array_filter([1, 2, 3, 4], fn($n) => $n % 2 === 0);

Arrays and Collections

$users = [
    ["name" => "Alice", "age" => 30],
    ["name" => "Bob", "age" => 25],
];

// Array methods
$names = array_map(fn($u) => $u["name"], $users);
$ages = array_column($users, "age");
$filtered = array_filter($users, fn($u) => $u["age"] > 25);

// Splice, push, pop
$stack = [];
array_push($stack, 1, 2, 3);
$last = array_pop($stack);  // 3

// Sorting
sort($names);            // by value, ascending
ksort($assoc);            // by key
usort($users, fn($a, $b) => $a["age"] <=> $b["age"]);  // custom compare

Control Flow

// Match (PHP 8) — more robust than switch
$status = 404;
$message = match($status) {
    200 => "OK",
    404 => "Not found",
    500 => "Server error",
    default => "Unknown",
};

// Null coalescing and null-safe
$name = $user["name"] ?? "Guest";  // if null or missing, use "Guest"
$city = $user["address"]["city"] ?? "Unknown";  // nested

// Null coalesce assignment
$name ??= "Unknown";  // assigns only if null

// Null-safe operator (PHP 8.0)
$name = $user?->profile?->name;  // if $user or profile is null, returns null

Modern PHP 8 Features

Named Arguments

function create_user(
    string $name,
    int $age = 18,
    bool $verified = false,
    array $tags = []
): array {
    return ["name" => $name, "age" => $age, "verified" => $verified, "tags" => $tags];
}

// Call in any order — no confusion
$user = create_user(
    name: "Alice",
    verified: true,
    tags: ["admin", "premium"]
);

Union Types

A parameter can accept multiple types:

function process_id(string|int $id): string {
    return (string)$id;
}

function handle_result(Result|string $res): void {
    if ($res instanceof Result) {
        $res->display();
    } else {
        echo $res;
    }
}

Attributes (Annotations)

Attributes provide metadata that reflection APIs can inspect. They replace docblock annotations:

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Controller {
    public function __construct(
        public string $route,
        public array $middleware = []
    ) {}
}

#[Route("/users", method: "GET")]
#[Controller(route: "/users")]
class UserController {
    public function index(): array {
        return ["users" => ["Alice", "Bob"]];
    }
}

Constructor Property Promotion

class Point {
    // Before PHP 8: declare properties, then assign in constructor
    // After PHP 8: declare + initialize in constructor signature
    public function __construct(
        public float $x,
        public float $y,
        private float $z = 0.0  // private promoted property
    ) {}
}

$p = new Point(x: 1.5, y: 2.5);
echo $p->x;  // 1.5

This eliminates boilerplate and makes class intent obvious at a glance.

Match Expression

$status = 404;
$text = match ($status) {
    200 => "OK",
    201 => "Created",
    404, 405 => "Not found",
    default => "Unknown",
};

match is expression-based (returns a value), strict (===) comparison, and exhaustive — missing cases will trigger errors at analysis time.

Enumerations (Enums)

#[Value("red")]
enum Color: string {
    case Red = 'red';
    case Green = 'green';
    case Blue = 'blue';

    public function hex(): string {
        return match ($this) {
            self::Red => '#ff0000',
            self::Green => '#00ff00',
            self::Blue => '#0000ff',
        };
    }
}

function describe(Color $c): string {
    return $c->value . ' => ' . $c->hex();
}

echo describe(Color::Red);  // "red => #ff0000"

Enums provide type safety — you can't pass a raw string where a Color is expected, and the compiler knows the full set of allowed values.

Mixed Type and Named Parameters

function process(mixed $data, int $timeout = 30): mixed {
    // mixed = any type (int, float, string, array, object, null)
    // Named parameters for readability
    return process($data, timeout: 60);
}

Readonly Properties

class Config {
    public readonly string $version;

    public function __construct() {
        $this->version = "2.0";  // can only be assigned once, in constructor scope
    }
}

Readonly properties provide immutability without making the whole object immutable.

Composer and Dependency Management

Composer is PHP's package manager. It manages dependencies via composer.json, resolves versions, installs packages, and generates autoload files.

Project Setup

# Initialize
composer init
# Interactive prompts create composer.json

# Require a package
composer require symfony/http-foundation

# Require for development only
composer require --dev phpunit/phpunit

# Install from lock file
composer install

# Update all (respects version constraints)
composer update

composer.json

{
    "name": "fogserv/user-api",
    "description": "User management API",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": ">=8.2",
        "symfony/http-foundation": "^6.4",
        "monolog/monolog": "^3.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.5",
        "phpstan/phpstan": "^1.10"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "config": {
        "sort-packages": true,
        "platform": {
            "php": "8.2"
        }
    },
    "minimum-stability": "stable"
}

The platform config lets you specify the PHP version you deploy to, so dependencies are resolved for that version, not your local machine.

Autoloading

Composer generates vendor/autoload.php. Use PSR-4 (directory-level) or Classmap autoloading:

// vendor/autoload.php handles this
require __DIR__ . '/vendor/autoload.php';

use App\Services\UserService;  // App\Services maps to src/Services/

$service = new UserService();

PSR-4 is preferred for large projects. Classmap is faster but requires regeneration (composer dump-autoload) when files change.

Scripts

Run custom commands with composer run:

{
  "scripts": {
    "test": "phpunit",
    "lint": "phpstan analyse src",
    "start": "php -S localhost:8000 -t public",
    "migrate": ["php bin/migrate.php"]
  }
}

Run with:

composer run test
composer run lint

Lock File and Reproducibility

composer.lock pins exact package versions. Always commit composer.lock for applications (not libraries, since libraries don't control which versions users install). This ensures every deploy uses the same dependency tree.

Guided Checkpoint

Install PHP and Composer, scaffold a project, and run a small CLI:

# Verify PHP 8.2+ and Composer are available
php --version    # must be 8.2 or later
composer --version

mkdir -p /tmp/php-checkpoint && cd /tmp/php-checkpoint
composer init --name="fogserv/checkpoint" --type="project" --no-interaction
composer require --dev phpstan/phpstan --no-interaction

mkdir -p src
cat > src/Greeter.php <<'EOF'
<?php
declare(strict_types=1);

namespace Fogserv\Checkpoint;

final class Greeter {
    public function __construct(
        private readonly string $prefix = "Hello"
    ) {}

    public function greet(string $name): string {
        return "{$this->prefix}, {$name}!";
    }
}
EOF

cat > check.php <<'EOF'
<?php
require __DIR__ . '/vendor/autoload.php';

$g = new \Fogserv\Checkpoint\Greeter();
echo $g->greet("fogserv") . "\n";
EOF

php check.php
# Expected output: Hello, fogserv!

3. Anti-Patterns & Common Pitfalls

These failure modes appear repeatedly in PHP codebases. Each is detectable, preventable, and rooted in PHP's dynamic typing or request-scoped execution model.

Anti-Pattern: Skipping declare(strict_types=1)

Without strict types, PHP coerces values silently. A function expecting int will accept the string "42" and convert it. A function expecting array will accept null and produce a warning rather than a TypeError:

// Bad: implicit coercion — bugs hide in production
function add(int $a, int $b): int {
    return $a + $b;
}
add("1", "2");  // returns 3, no error

// Good: strict types — coercion throws
declare(strict_types=1);
function add(int $a, int $b): int {
    return $a + $b;
}
add("1", "2");  // TypeError: must be of type int

Fix: add declare(strict_types=1); as the very first line of every PHP file. It is a one-character protection that prevents entire categories of bugs.

Anti-Pattern: Storing State in Globals Between Requests

Because each request is isolated, globals reset every time. Code that depends on shared state across requests — global caches, in-memory counters, instance singletons — produces inconsistent behavior:

// Bad: state is lost between requests
$GLOBALS['request_count'] = ($GLOBALS['request_count'] ?? 0) + 1;
echo "Request #{$GLOBALS['request_count']}";

Fix: use external storage for state that must persist: Redis, Memcached, database, file-based session. For per-request caches, use a local variable or a scoped service.

Anti-Pattern: Trusting User Input Without Validation

$_GET, $_POST, $_COOKIE, and $_REQUEST are untyped arrays of strings. Using them directly in queries, file paths, or output produces SQL injection, path traversal, and XSS:

// Bad: SQL injection
$name = $_POST['name'];
$db->query("SELECT * FROM users WHERE name = '$name'");

// Good: parameterized query
$stmt = $db->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute(['name' => $_POST['name']]);

Fix: validate every input. Use filter_input for basic type coercion, parameterized queries for SQL, and htmlspecialchars for any output that may contain user data.

Anti-Pattern: Using switch Where match Is Cleaner

match (PHP 8) is expression-based, strict (===), and supports multiple labels. switch is statement-based, uses loose comparison (==), and falls through by default — all of which are sources of subtle bugs:

// Bad: switch with fallthrough and loose comparison
switch ($status) {
    case 200:        // matches 200, but also "200", 200.0, etc.
        $msg = "OK";
        break;
    case 404:        // missing break — falls through!
    case 405:
        $msg = "Not found";
        break;
}

// Good: match is strict, exhaustive, and an expression
$msg = match ($status) {
    200 => "OK",
    404, 405 => "Not found",
    default => "Unknown",
};

Fix: prefer match for value-to-value mapping. Use match even for simple cases — it is clearer and prevents future bugs when new cases are added.

Anti-Pattern: Storing Complex Data in $_SESSION Without Serialization Awareness

$_SESSION values are serialized between requests. Storing closures, resource handles, or objects with circular references produces warnings or data loss:

// Bad: closure in session — cannot be serialized
$_SESSION['handler'] = function() { /* ... */ };

// Better: store a string identifier and reconstruct on use
$_SESSION['handler_id'] = "default";
$handler = match ($_SESSION['handler_id']) {
    "default" => new DefaultHandler(),
    "advanced" => new AdvancedHandler(),
};

Fix: store simple types (int, string, array, serializable objects) in $_SESSION. Use serialize()/unserialize() or json_encode/json_decode to control the format.


4. Independent Challenge

Design a small request router using modern PHP 8 features, without step-by-step guidance.

Without step-by-step guidance, produce:

  1. A Router class that maps HTTP method + path patterns to handler callables. Use attributes (PHP 8) or constructor-style registration.
  2. Support path parameters, e.g., /users/{id} should match /users/42 and extract id=42 as a parameter.
  3. Support middleware: each route has a list of middlewares that wrap the handler.
  4. A dispatch(string $method, string $path): Response method that returns a 404 response if no route matches.
  5. Use a Response class with constructor property promotion for status, headers, and body.

Constraints:

Deliverable: a Router.php file with a routes.php configuration example and a public/index.php that dispatches a sample request. Verify by running the built-in PHP server (php -S localhost:8000) and curling each route. No solution is provided.


5. Consolidation & Key Invariants


6. Next Steps

Next Article: typescript-basics — for full-stack projects where the frontend and backend share types, or language-comparison for a broader overview.


Content depth matches kb/containers/docker-concepts.md — production-level patterns, real syntax, and practical examples.

Change Log

Date Change
2026-09-15 Migrated to canonical 6-section template (Architectural Overview, Deep Dive with Guided Checkpoint, Anti-Patterns, Independent Challenge, Consolidation, Next Steps)
2026-08-29 Full hydration of PHP Basics lesson
2026-08-29 Added sections: Why PHP, Basic Syntax, Modern PHP 8 Features, Composer
2026-09-15 Added Next Steps links, preserved prerequisites and tags

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse