Flat-file PHP CMS.
No database.

PhlatPage is a flat-file PHP CMS where every page is a folder and every piece of content is a JSON file — no database required.

No database

Every page is a folder, every piece of content is a JSON file. No connection strings, no migrations, no query language.

Override system

Everything in phlat/ is a default. Drop a matching file in site/ and it takes precedence. No configuration required.

Hypermedia-first

Routes return HTML. HTMX consumes fragments at clean, readable URLs. The URL pattern is the API — no JSON layer needed.

Version controlled

Content lives alongside code in plain files. Every change is a diff. Roll back a page edit the same way you roll back a commit.

Quick start

Point the web server document root at public/, then bootstrap the app.

```php
<?php

$app = new Phlat\App(__DIR__);
$app->execute();
```

Recent articles

All articles →

Writing Testable Php

Testable PHP is PHP where dependencies are injectable, side effects are isolated, and pure logic is separated from I/O. A function that reads a file, transforms the data, and writes a result is testing three concerns at once. A function that only transforms data is testable without any filesystem setup.

The biggest obstacle to testability is new inside methods — constructing dependencies rather than receiving them. Dependency injection (passing dependencies in) allows tests to substitute fakes or stubs. Static methods and global state ($_SESSION, $_GET) are harder to isolate; wrapping them in injectable classes or functions solves this.

Writing Readable Php

Readable PHP names things clearly, avoids clever idioms that require deciphering, and structures logic as a series of small, named steps. A function that does three things should usually be three functions. A variable named $d should usually be $date. A condition embedded in a return statement should usually be a named variable with a clear boolean name.

The test: would a developer who didn't write this code understand it in thirty seconds? If not, the issue is usually naming or structure, not comments. A well-named function requires no docblock to explain what it does; a poorly named function requires a docblock that still leaves questions.

Writing For The Web In 2025

Web readers scan before they read. They look for headings, bold text, lists, and short paragraphs to assess whether the page is worth reading. Dense blocks of text are abandoned. The discipline of web writing is front-loading information: state the conclusion first, then support it.

Short sentences. One idea per paragraph. Active voice. Headings that summarise rather than tease. These aren't rules invented for the web — they're the same advice as good expository writing, applied to a context where the reader has even less patience and more alternatives.

Writing A Simple Router In Php

A router maps URL patterns to handler functions. At its simplest: an array of [method, pattern, handler] tuples, iterated on each request until a match is found. Matching a route means checking the HTTP method and comparing the URL path against a pattern that may include variable segments.

Variable segments — :slug, (:segment) — are converted to regex captures. The first matching route wins; if no route matches, return a 404. The handler receives captured route parameters as arguments and returns a response.

function match(string $method, string $path, array $routes): ?array {
    foreach ($routes as [$m, $pattern, $handler]) {
        if ($m !== $method) continue;
        $regex = preg_replace('/\(:segment\)/', '([^/]+)', $pattern);
        if (preg_match("#^{$regex}$#", $path, $matches)) {
            return [$handler, array_slice($matches, 1)];
        }
    }
    return null;
}