Files
xamxam/docs/development.md
T

7.7 KiB

Development Guide

Setup, workflow, building assets, and testing for XAMXAM development.


Requirements

  • PHP ≥ 8.4 with ext-json, ext-openssl, ext-pdo (and ext-sqlite3 for the local DB)
  • Composer for PHP dependencies
  • Node.js / npm for frontend asset builds (rolldown, lightningcss, biome)
  • SQLite3 CLI (for just query, just init-db)

One-time setup

From the repo root, run the provisioning script:

just provision

This is idempotent and safe to re-run. It:

  1. Ensures app/.env has an APP_KEY — never overwrites an existing key (it prints a message and keeps the current one so encrypted credentials stay decryptable).
  2. Installs Composer deps (composer install) and JS deps (npm ci).
  3. Runs the DB schema + migrations (just migrate).
  4. Checks the first-admin-password state and tells you if /admin/ is still unauthenticated.

If you prefer to run the steps manually, or just isn't installed yet:

composer install           # PHP deps (vendor/)
npm ci                     # JS build deps (node_modules/)
just migrate               # create/update the SQLite DB from schema + migrations

just setup exists but its backing script (scripts/setup-dev.sh) is stale — it still clones the old php-live-reload library and creates legacy admin/data/ directories. Live-reload now ships inside the app (app/public/live-reload.php), and assets are built with rolldown/lightningcss, not the live-reload watcher. Prefer just provision.

APP_KEY and the app/.env file

The app derives DB/storage paths from the filesystem layout, so the public site and admin panel run without any env file. But Crypto (decrypting the SMTP password, PeerTube credentials, and Nextcloud WebDAV sync) requires an app/.env containing an APP_KEY:

APP_KEY=<base64-encoded 32 random bytes>

Generate one manually with:

php -r 'echo "APP_KEY=" . base64_encode(random_bytes(32)) . PHP_EOL;' > app/.env
chmod 600 app/.env

app/.env must never be committed. just provision only creates app/.env (or appends APP_KEY) when the key is absent — if the value already exists it is left untouched so existing encrypted credentials (SMTP password, PeerTube OAuth, Nextcloud WebDAV) remain decryptable. If the file is missing, the public site still runs but any path that reads encrypted credentials throws RuntimeException: APP_KEY not found.

First admin login

A brand-new DB seeds site_settings.admin_password_hash as an empty string, which makes AdminAuth::requireLogin() a pass-through — i.e. /admin/ starts unauthenticated until a password is set. To secure it, open http://127.0.0.1:8000/admin/account (or the production admin) and set the admin password, which stores the bcrypt hash in site_settings.

Project Structure

xamxam/
├── app/                      # All application code (the project root on the server)
│   ├── bootstrap.php         # App bootstrapping (constants, autoload, config)
│   ├── router.php            # Router for the PHP built-in dev server
│   ├── public/               # DocumentRoot (web-accessible only)
│   │   ├── index.php         # Front controller / entry point
│   │   ├── request-access.php
│   │   ├── live-reload.php
│   │   ├── admin/            # Admin panel
│   │   ├── partage/          # Student submission via share links
│   │   └── assets/           # Built CSS/JS, fonts, images (dist/ is generated)
│   ├── src/                  # PHP classes
│   │   ├── Controllers/      # Request controllers (Home, Search, Tfe, Export, …)
│   │   ├── Form/             # Form helper
│   │   ├── Database.php      # DB access + queries
│   │   ├── AdminAuth.php     # Session auth for admin
│   │   ├── RateLimit.php
│   │   ├── FilepondHandler.php
│   │   ├── PeerTubeService.php
│   │   └── …                 # (see list in src/)
│   ├── templates/            # Template partials (public, admin, partials, partage)
│   ├── storage/              # Database + uploads (private)
│   │   ├── xamxam.db         # SQLite database
│   │   ├── schema.sql        # Base schema
│   │   ├── theses/ tfe/      # Uploaded files
│   │   └── cache/ logs/      # Runtime data
│   └── migrations/           # Migration runner + applied migrations
├── scripts/                  # Build / deploy / utility scripts
├── tests/phpunit/            # PHPUnit tests
├── nginx/                    # Server configuration + docs
├── deploy/                   # Cron configs (backup, cleanup)
├── justfile                  # Task runner
├── composer.json / package.json
└── TODO.md

Development Workflow

Start the dev server

just dev

This runs a one-shot asset build, applies pending migrations, opens http://127.0.0.1:8000/ (public) and /admin/ (admin), then starts the PHP built-in server (php -S 127.0.0.1:8000) with a chokidar watcher that rebuilds CSS/JS on change.

  • Public site: http://127.0.0.1:8000/
  • Admin panel: http://127.0.0.1:8000/admin/

Stop it with just stop.

If you only need the server without browsers/ui, run just dev-build first, then start the PHP built-in server manually with the php -d … -S command shown in the dev recipe (or add a just serve alias locally).

Frontend assets

Source CSS lives in app/public/assets/css/, source JS in app/public/assets/js/app/. They are bundled and minified into app/public/assets/dist/ by scripts/build*.mjs (rolldown + lightningcss).

just dev-build        # one-shot build (quicker output than full)
just build            # full build
just build-css        # CSS only
just build-js         # JS only
just build-check      # verify build output is up to date

There is no live-reloading in production — app/public/live-reload.php only activates under the PHP built-in server.

Testing

PHPUnit is used. Configuration lives in phpunit.xml, tests in tests/phpunit/ (with tests/bootstrap.php and tests/TestDatabase.php helpers).

just test              # run all PHPUnit tests
just test-coverage     # run with HTML coverage into coverage/

Linting / formatting

just lint-php     # phpstan (static analysis) + php-cs-fixer (coding standards)
just lint-css     # biome lint on app/public/assets/css/
just lint-js      # biome lint on app/public/assets/js/app/ + scripts/
just lint         # all linters
just fix          # auto-fix (biome + php-cs-fixer)

Database Operations

just migrate        # run pending migrations
just init-db        # create/reset DB from app/storage/schema.sql
just reset-db       # rm DB + init-db
just query          # open an interactive SQLite shell
just backup         # SQL dump into app/storage/backup_<timestamp>.sql
just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/
just cleanup-drafts [--no-dry-run]   # remove orphaned drafts > 7 days (default)
                                     # OLDER_THAN_HOURS=24 to change the threshold

See database.md for the full schema reference.

Deployment

See deployment.md. Files are pushed to the server with just deploy (rsync → xamxam:/var/www/xamxam/). There is no git repo on the remote.

Troubleshooting

just logs        # tail the dev error log (error.log)
just stop        # kill the dev server / asset watcher

If the browser doesn't hot-reload, confirm the asset watcher is still running and the rebuild succeeded (just dev-build).