- PHP 90%
- CSS 6.7%
- Dockerfile 1.8%
- Hack 1.3%
- Shell 0.2%
|
|
||
|---|---|---|
| .github/workflows | ||
| data | ||
| html | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yaml | ||
| LICENSE | ||
| README.md | ||
LEPP Core
A small starter for PHP sites on Linux, Nginx (the "E"), Postgres and PHP, run with Docker Compose. No framework, no ORM, no Composer packages: a router of about 160 lines, a PDO wrapper of about 60, plain PHP views, and SQL files you write yourself.
It is a scaffold to copy and build on, not a framework. The routing style ($param segments and plain function handlers) is inspired by phprouter.
Quick start
You need Docker with the Compose plugin.
cp .env.example .env
docker compose up --build
Then open http://localhost for the app and http://localhost:15432 for pgAdmin (log in with PGEMAIL and PGPASSWORD from .env). Postgres and pgAdmin only listen on 127.0.0.1, and PHP-FPM is only reachable from the nginx container.
The values in .env.example are throwaway development defaults. Change them before exposing anything beyond your own machine.
What is in the box
| Piece | Where | Notes |
|---|---|---|
| Router | html/core/Router.php |
get, post, put, patch, delete, any, $param segments, before and after hooks, automatic 404 |
| Database | html/core/DB.php |
PDO with prepared statements: DB::query, one, all, transaction |
| Views | html/views/ |
Plain PHP with shared partials; e() escapes output |
| API example | html/api/ping.php |
JSON endpoint at /api/{id} |
| CSRF | Router::set_csrf() |
Hidden input for forms, checked with Router::is_csrf_valid() |
| Logging | log_line() |
Request log written to LOG_DIR, outside the web root |
| Tests | tests/run.php |
No dependencies, see below |
Add a page
// html/core/routes.php
Router::get('/about', fn() => render_view('/about.php', ['title' => 'About']));
Router::get('/users/$id', function ($id) {
$user = DB::one('SELECT * FROM users WHERE id = ?', [$id]);
$user ? render_view('/user.php', ['user' => $user]) : http_response_code(404);
});
Always escape values in views with <?= e($value) ?>, and put <?php Router::set_csrf(); ?> inside every POST form (see html/views/home.php and FormController).
Add tables
Put your schema in data/scripts/tables.sql and any migration logic in data/scripts/initial-migration.sh. They are mounted into the database container at /var/www/scripts. There is no migration framework on purpose.
How requests flow
nginx serves static files from html/ and sends everything else to html/index.php. Direct requests to any other .php file return 403. index.php loads the helpers, middleware and routes, and the router runs the first match.
nginx -> index.php -> utils/functions.php -> core/middleware.php -> core/routes.php -> controller -> view
Configuration
| Setting | Where | Purpose |
|---|---|---|
POSTGRES_* |
.env |
Database user, password, name, host and port |
PGEMAIL, PGPASSWORD, PGPORT |
.env |
pgAdmin login and local port |
TIME_ZONE |
.env |
PHP time zone |
LOG_DIR |
compose php service |
Where request logs go (default /var/log/php, a named volume) |
data/php/app.ini |
repo | PHP overrides on top of the official php.ini-production |
Security notes
- Output is escaped with
e(), queries use prepared statements, and forms are CSRF protected. - Session cookies are HttpOnly and SameSite=Lax (and Secure over HTTPS).
- nginx hides its version and sends
X-Content-Type-Options,X-Frame-OptionsandReferrer-Policy. - There is no TLS, login or user table here. Put a TLS-terminating proxy in front for anything public, and add authentication before storing user data.
Tests
You need PHP 8.2 or newer with pdo_sqlite:
php tests/run.php
The runner starts PHP's built-in server and makes real HTTP requests, so it checks routing, 404 pages, CSRF rejection, form validation and escaped output end to end. Database helpers are tested against in-memory SQLite. CI runs a syntax check and the tests on every push.
The Docker setup (compose file, Dockerfile and nginx config) is not covered by the test runner.
License
MIT, see LICENSE.