CodeBlanks Logo Icon CodeBlanks

Challenge - Full PHP app

PHP course · Expert · Lesson 54/54

Build a modern task management system with readonly, enum, match, fibers and Docker.

Modern PHP: readonly + enum + match + Fiber

In the final lesson you crown every layer with modern PHP features. enum TaskStatus: string defines task states as a fixed set of values. readonly class Task gives an immutable model after construction; properties are locked once set.

The match expression produces a label per case; it is shorter than switch and strictly typed. Fiber adds cooperative scheduling: a routine can pause with Fiber::suspend() and resume with resume(). With php_fpm it is ready for production in Docker.

Cooperative scheduling with Fiber

Fiber is PHP 8.1's cooperative routine primitive. new Fiber($fn) wraps a routine; start() begins it, Fiber::suspend() pauses it and resume() continues. isSuspended() queries the state.

The FiberScheduler collects fibers in an array; run() starts each and calls resume() while isSuspended() is true. You get thread-like sequential execution on a single thread. With php_fpm and Docker the application is ready to ship; the combination of readonly + enum + match + Fiber forms the core of the mini framework.

Exercise Write the modern PHP core

The starting code has the ETL pipeline, ReflectionSerializer, Cache, benchmark, ReportService, RateLimiter, CSP, SessionGuard, needsRehash and validateEmail ready. Now add the modern PHP core.

Define enum TaskStatus: string with Pending = "pending", InProgress = "in_progress", Done = "done". The readonly class Task exposes id, title and status as promoted public properties; default to TaskStatus::Pending. statusLabel(TaskStatus $s): string uses match to return an English label per case. The FiberScheduler class adds new Fiber in add(callable); run() calls start() on each fiber and calls resume() while isSuspended() is true. The demo prints a task, adds two fibers and runs them.

Show example solution
string with Pending, InProgress, Done cases. The readonly class Task has id, title, status as promoted public properties. statusLabel(TaskStatus $s): string uses match() to return an English label for each case. FiberScheduler adds new Fiber in add(callable); run() starts each fiber and calls resume() while it isSuspended().
Example solution:
<?php
// ETL + Reflection + Performance + Security + Modern: write enum + readonly + match + Fiber

function readRows(array $rows): Generator {
    foreach ($rows as $row) {
        yield $row;
    }
}

function transform(iterable $rows, callable $fn): Generator {
    foreach ($rows as $row) {
        yield $fn($row);
    }
}

function pipeline(array $rows, callable $fn): Generator {
    yield from transform(readRows($rows), $fn);
}

function loadAll(iterable $rows): array {
    $out = [];
    foreach ($rows as $row) {
        $out[] = $row;
    }
    return $out;
}

#[Attribute]
class Field {
    public function __construct(public string $name = "") {}
}

class Product {
    public function __construct(
        #[Field("id")] public int $id,
        #[Field("name")] public string $name,
    ) {}
}

class ReflectionSerializer {
    public function serialize(object $obj): array {
        $rc = new ReflectionClass($obj);
        $out = [];
        foreach ($rc->getProperties() as $prop) {
            $attrs = $prop->getAttributes(Field::class);
            if (empty($attrs)) continue;
            $field = $attrs[0]->newInstance();
            $key = $field->name !== "" ? $field->name : $prop->getName();
            $out[$key] = $prop->getValue($obj);
        }
        return $out;
    }
}

class Cache {
    private array $store = [];
    public function get(string $key, callable $loader) {
        if (!isset($this->store[$key])) {
            $this->store[$key] = $loader();
        }
        return $this->store[$key];
    }
}

function benchmark(callable $fn): array {
    $start = hrtime(true);
    $result = $fn();
    $elapsed = (int)((hrtime(true) - $start) / 1000000);
    $mem = memory_get_usage(true);
    return ["result" => $result, "ms" => $elapsed, "bytes" => $mem];
}

class ReportService {
    private PDO $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
        $this->pdo->exec("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category_id INTEGER)");
        $this->pdo->exec("CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT)");
        $this->pdo->prepare("INSERT INTO categories (name) VALUES (?)")->execute(["kirtasiye"]);
        $this->pdo->prepare("INSERT INTO products (name, category_id) VALUES (?, ?)")->execute(["kale", 1]);
        $this->pdo->prepare("INSERT INTO products (name, category_id) VALUES (?, ?)")->execute(["defter", 1]);
    }
    public function eagerReport(): array {
        $sql = "SELECT p.name AS product, c.name AS category FROM products p JOIN categories c ON p.category_id = c.id ORDER BY p.id";
        return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
    }
}

class RateLimiter {
    private array $counts = [];
    public function __construct(private int $limit = 3) {}
    public function allow(string $key): bool {
        $this->counts[$key] = ($this->counts[$key] ?? 0) + 1;
        return $this->counts[$key] <= $this->limit;
    }
}

function applyCsp(): void {
    header("Content-Security-Policy: default-src 'self'");
}

class SessionGuard {
    public function regenerate(): void {
        session_regenerate_id(true);
    }
}

function needsRehash(string $hash): bool {
    return password_needs_rehash($hash, PASSWORD_DEFAULT);
}

function validateEmail(string $email): bool {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

enum TaskStatus: string {
    case Pending = "pending";
    case InProgress = "in_progress";
    case Done = "done";
}

readonly class Task {
    public function __construct(
        public int $id,
        public string $title,
        public TaskStatus $status = TaskStatus::Pending,
    ) {}
}

function statusLabel(TaskStatus $s): string {
    return match($s) {
        TaskStatus::Pending => "PENDING",
        TaskStatus::InProgress => "INPROGRESS",
        TaskStatus::Done => "DONE",
    };
}

class FiberScheduler {
    private array $fibers = [];
    public function add(callable $fn): void {
        $this->fibers[] = new Fiber($fn);
    }
    public function run(): void {
        foreach ($this->fibers as $f) {
            $f->start();
            while ($f->isSuspended()) {
                $f->resume();
            }
        }
    }
}

// Demo:
$task = new Task(1, "write summary", TaskStatus::InProgress);
echo "task: " . $task->title . "\n";
echo "status: " . statusLabel($task->status) . "\n";

$sched = new FiberScheduler();
$sched->add(function() {
    echo "fiber1: started\n";
    Fiber::suspend();
    echo "fiber1: resumed\n";
});
$sched->add(function() {
    echo "fiber2: started\n";
});
$sched->run();
echo "php_fpm: ready\n";

Test yourself

  1. Match modern PHP concepts

    enum
    Named value set
    readonly
    Immutable after init
    match
    Pattern comparison
    Fiber
    Cooperative routine
  2. What does a readonly property provide?

    • Making the property serializable
    • Preventing modification after first assignment
    • Auto-populating the property
    • Making the property static
    Show answer

    Preventing modification after first assignment — Correct. A readonly property cannot be modified after its first assignment; ideal for immutable value objects.

  3. Complete the match expression

    return ____($s) {
        TaskStatus::Pending => "PENDING",
        TaskStatus::InProgress => "INPROGRESS",
        TaskStatus::Done => "DONE",
    };
    Show answer
    return match($s) {
        TaskStatus::Pending => "PENDING",
        TaskStatus::InProgress => "INPROGRESS",
        TaskStatus::Done => "DONE",
    };

All lessons in Expert