Table of contents
TCH099
TCH099 is a small PHP web application built as a demo to deploy on Azure. It serves a few HTML pages, a JSON API for sign-up, login, logout and user details, and an admin page that lists every account; an Android app talks to the same API. Its documentation site is written for students starting a team project: user stories, the API reference, a browser DevTools primer, the Android setup and the Azure deployment steps, all in French.
1. Overview
There is no framework. Two files do the routing, a handful of scripts in api/ do the work, and one Clients table holds the accounts.
- Routes.
routes.phplists every route as a call such asget('/login', 'frontend/pages/login.html')orpost('/api/login', 'api/login.php').router.phpdefines those functions and matches the request. - Pages.
frontend/pages/holds static HTML (home, login, sign-up, hello world, random image, admin, 403, 404, 50x) that call the API withfetch. - API.
api/login.php,signup.php,logout.php,user_details.php,clients.php,hello-world.phpandrandom-image.phpread JSON bodies and answer with JSON. Shared helpers (loginUser,registerUser,isAdmin, CSRF helpers) live inapi/utils/utils.php. - Data.
config.phpopens a PDO connection to MySQL/MariaDB.db/1create.sqlcreatesClients(id, unique username and email, password hash,role ENUM('admin','client'), timestamps);db/db.pyruns the three SQL files with credentials from.env. - Mobile.
MyMobileApp/is an Android Studio project with login, sign-up and user-details activities.HttpClient.javakeeps one OkHttp client with a cookie jar so the PHP session cookie survives between calls.
2. Use cases
PHP and MariaDB demo web app with an Android client, used to teach routing, sessions and Azure deployment.
- Create an account
- Web or mobile userMariaDB database
The signup page or SignupActivity on Android posts to /api/signup; api/signup.php calls registerUser(), which inserts the account into the Clients table created by db/1create.sql.
- Log in with a session
- Web or mobile userMariaDB database
login.html and LoginActivity post the username and password to /api/login; api/login.php checks them with loginUser() and opens a PHP session, and /api/logout closes it.
- Show a random image
- Web or mobile user
random-image.html calls /api/random-image, which returns a cat picture URL from TheCatAPI or a thispersondoesnotexist.com URL depending on imageType.
- Get a greeting by language
- Web or mobile user
/api/hello-world/$langue answers with a greeting in French, English, Spanish or German, as a minimal API example.
- List clients as admin
- AdministratorMariaDB database
The /admin route serves admin.html only when isAdmin() is true; the page loads /api/clients, which reads the Clients table for a session whose role is admin.
2.1. Scenario: Run the demo locally and reach the admin page
- Fill the DB_HOST and MYSQL_* variables listed in the README, then run docker-compose up -d --build.
- Load the schema and seed data with python3 db/db.py, or import db/1create.sql, 2contraines.sql and 3insert.sql in phpMyAdmin on port 8060.
- Open http://localhost:8001/signup, create an account, then log in at /login.
- With an admin account, open /admin to see the Clients table loaded from /api/clients.
- Push to main to let .github/workflows/main_tch099antoine.yml zip the app and deploy it to the Azure Web App.
Files readREADME.mdroutes.phpdocker-compose.ymlDockerfileapi/login.phpapi/clients.phpapi/random-image.phpdb/db.pyMyMobileApp/MyApplication/app/build.gradle.kts.github/workflows/main_tch099antoine.ymlapi/utils/utils.php
3. What the documentation site covers
The GitHub Pages site is docs/index.html, published by the static.yml workflow. It is organised as the steps of a team project:
- Plan. One epic, “account and profile management”, split into seven user stories (sign-up, login, logout, hello-world message with the user’s name, random image, user details, admin login), each with its sub-tasks: design the form, write the backend logic, hash passwords, test.
- Project structure. An annotated tree of
api/,db/,frontend/, the router files and the deployment files. - Database. The
.envvariables and thedb.pyinitialisation commands. - API. Endpoint, method, request body and response for each route, with a PlantUML sequence diagram per call.
- Front end. A DevTools tour (Elements, Console, Sources, Network, Performance, Memory, Application, Security) with screenshots and shortcuts.
- Mobile. Android Studio setup, the
buildConfigFieldAPI URLs, the layout files, and why the emulator reaches the host at10.0.2.2. - Deployment. A deployment diagram and seven Azure portal steps, from creating the Web App and database to SSH-ing into the container and writing its
.env. The README adds an eighth step on private links.
4. Architecture
flowchart LR
B["Browser pages<br/>frontend/pages/*.html"] -->|fetch JSON| W
A["Android app<br/>OkHttp + cookie jar"] -->|JSON over HTTPS| W
subgraph W["Web server"]
N["nginx on Azure<br/>try_files → routes.php"]
H["Apache in Docker<br/>.htaccess → router.php"]
end
W --> R["router.php + routes.php<br/>first matching route wins"]
R -->|HTML route| P["Static page"]
R -->|API route| S["api/*.php"]
S --> U["api/utils/utils.php<br/>session and auth helpers"]
U --> C["config.php<br/>PDO connection"]
C --> D[("MariaDB / MySQL<br/>Clients table")]
S -->|random-image| X["TheCatAPI"]
Two web servers lead to the same router. The Docker image is php:8.1-apache; its .htaccess sends every request that is not a static file (image, CSS, JS, SVG, ZIP or PDF) to router.php, which ends with require_once 'routes.php'. On Azure App Service (PHP 8.2), nginx serves existing files and falls back to routes.php, which starts with require_once 'router.php'. Either way both files load and the route list runs once.
5. How the router matches a request
get(), post(), put(), patch(), delete() and any() each check the HTTP method and then call route($route, $target). Because routes.php calls them one after another, the first route that matches includes its target and calls exit(); nothing after it runs. route() works like this:
- If the target is a string without
.phpor.html, it appends.php. - It cleans the request URI, strips a trailing slash and the query string, and splits both the route and the URI on
/. - A route ending in
*matches any longer path and passes the rest as one argument. - Otherwise the segment counts must be equal. A segment starting with
$captures the URI segment into a PHP variable of that name ($$route_part = …); any other segment must match exactly. - On a match it calls the closure with the captured values, or
include_onces the file, then exits.
The last line of routes.php is any('/404', …), and route() short-circuits /404 to the 404 page without comparing anything. Every request that fell through all earlier routes therefore gets the 404 page.
A worked example, POST /api/hello-world/es with {"nom": "Tremblay", "prenom": "Ana"}:
get()calls skip it (wrong method);post('/api/login', …)and the others fail on segment count or text.post('/api/hello-world/$langue', 'api/hello-world.php')has three segments like the URI.apiandhello-worldmatch, and$languecaptureses.hello-world.phpis included insideroute(), so it sees$langue = 'es', readsnomandprenomfrom the body (or uses the logged-in username asprenom), and returns{"nom": "Tremblay", "prenom": "Ana", "langue": "es", "message": "Hola Mundo Tremblay Ana!"}.
The same URL with GET behaves differently: the GET branch of hello-world.php overwrites $langue with $_GET['langue'] ?? 'fr', so the path segment is ignored and the answer is in French unless ?langue=es is added.
6. Login and roles
sequenceDiagram
participant Page as login.html
participant Router as routes.php
participant Login as api/login.php
participant Utils as utils.php
participant DB as Clients table
Page->>Router: POST /api/login {"username","password"}
Router->>Login: include (first matching route)
Login->>Utils: loginUser(username, password)
Utils->>DB: SELECT * FROM Clients WHERE username = ?
DB-->>Utils: row with password hash and role
Utils->>Utils: password_verify(), then $_SESSION['user_details'] = {username, email, role}
Login-->>Page: {"status":"success","redirect":"/home"}
Page->>Router: GET /admin
Router->>Utils: isAdmin() reads role from the session
Router-->>Page: admin.html, or 403.html for a client
registerUser hashes the password with password_hash(PASSWORD_DEFAULT) and only accepts the roles admin and client. Every query uses a prepared statement. The session, not a token, carries the identity, which is why the Android client needs a cookie jar.
7. Team workflow and delivery
The README’s developer guide gives the team’s working rules: how to add a route, a page and an API endpoint, and five good practices (separate concerns, validate input on the server, return proper HTTP errors, reuse utils.php, use prepared statements and escape HTML). The user stories on the documentation site double as the backlog.
flowchart LR
Dev["Push to main"] --> CI["main_tch099antoine.yml<br/>build job"]
CI --> Z["setup-php 8.2, composer if present,<br/>zip everything except MyMobileApp/"]
Z --> DEP["deploy job<br/>azure/login + webapps-deploy"]
DEP --> AZ["Azure Web App tch099antoine<br/>startup: copy nginx.conf, reload nginx"]
Dev --> PG["static.yml"] --> GP["GitHub Pages<br/>docs/ folder"]
Two workflows run on each push to main. main_tch099antoine.yml zips the PHP app (the Android project is excluded) and deploys it to the App Service named tch099antoine with azure/webapps-deploy. static.yml uploads docs/ to GitHub Pages. On the Azure side, the startup command copies the repository’s nginx.conf over the default site and reloads nginx, so that unknown paths reach routes.php.

8. Getting started
Create a .env with the variables from the README (DB_HOST=tch099-db, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, MYSQL_ROOT_PASSWORD), then start the stack:
docker compose up -d --build
cd db
python3 -m pip install mysql-connector-python python-dotenv
python3 db.py
Compose starts three containers on one bridge network: web (the Apache image, port 8001, with the repository mounted at /var/www/html), db (MariaDB, port 3306, data in the dbdata volume) and phpmyadmin (port 8060). Open http://localhost:8001.
9. Limits
config.phphard-codes the local credentials (tch099-db,user/password) and never reads the environment;$isCloudEnvis computed and unused. The Azure guide works around this by writing a.envover SSH, but PHP does not load it.config.phpechoes “Database connection successful.” on every include. API responses therefore start with that text before the JSON, which makesresponse.json()in the pages reject.api/signup.phptakesrolefrom the request body, so anyone can create anadminaccount and then open/adminand/api/clients.api/clients.phpreturnsSELECT * FROM Clients, password hashes included.router.phpsetsAccess-Control-Allow-Origin: *and turns ondisplay_errorsfor every request.- The README’s “add a route” section shows a
$routes['/path'] = [...]array format thatroutes.phpdoes not use; the real format is theget()/post()calls above. - The CSRF helpers in
utils.phpare not called by any API script.
10. Sources
- GitHub repository
- Documentation site
- README
router.phproutes.phpapi/utils/utils.phpapi/hello-world.phpconfig.php.github/workflows/main_tch099antoine.ymldocker-compose.yml
11. CI/CD pipeline
2 GitHub Actions workflows, 3 jobs. Each box is a job; double-bordered boxes publish or deploy, and arrows follow needs.
Mermaid Pipeline flow
flowchart LR
subgraph wf0["Build and deploy PHP app to Azure Web App -…"]
direction LR
wf0_build[["build<br/>ubuntu-latest"]]
wf0_deploy[["deploy<br/>ubuntu-latest"]]
end
subgraph wf1["Deploy static content to Pages"]
direction LR
wf1_deploy[["deploy<br/>ubuntu-latest"]]
end
t0(["push · manual run"])
t1(["Runs · on · pushes · targeting · the · defa…"])
d0[("GitHub Pages")]
t0 --> wf0_build
wf0_build --> wf0_deploy
t1 --> wf1_deploy
wf1_deploy -.-> d0
| Workflow | Triggers | Jobs | Publishes to |
|---|---|---|---|
Build and deploy PHP app to Azure Web App - tch099antoine.github/workflows/main_tch099antoine.yml | push, manual run |
| — |
Deploy static content to Pages.github/workflows/static.yml | Runs, on, pushes, targeting, the, default |
| GitHub Pages |
Generated from the source at commit a7323c8.
12. Deployment diagram
Mermaid Deployment diagram
flowchart LR
repo[("GitHub repository")]
ci[["GitHub Actions<br/>2 workflows"]]
image["Docker image<br/>FROM php:8.1-apache"]
pub_GitHub_Pages[("GitHub Pages")]
subgraph compose["Docker Compose"]
svc_web["web"]
svc_db["db"]
svc_phpmyadmin["phpmyadmin"]
end
repo -->|"push or pull request"| ci
ci -->|"docker build"| image
ci -->|"publishes"| pub_GitHub_Pages
image -->|"docker compose up"| compose
13. External dependencies
8 declared packages (8 runtime, 0 development) in MyMobileApp/MyApplication/app/build.gradle.kts.
Package table 8
| Package | Version | Scope | Purpose | Registry |
|---|---|---|---|---|
appcompat | — | runtime | — | JVM (Gradle) |
material | — | runtime | — | JVM (Gradle) |
activity | — | runtime | — | JVM (Gradle) |
constraintlayout | — | runtime | — | JVM (Gradle) |
okhttp | — | runtime | HTTP client | JVM (Gradle) |
okio | — | runtime | — | JVM (Gradle) |
dotenv | — | runtime | Configuration | JVM (Gradle) |
loggingInterceptor | — | runtime | Logging and observability | JVM (Gradle) |
14. Glossary
Repository
The versioned source files, configuration, and history of a project.
README
The repository’s entry document, usually covering its purpose and initial setup.
Dependency
A library, tool, or service that another component needs to build or run.
API
An interface through which software components exchange requests and results. An API does not have to use HTTP.
HTTP
A request-and-response protocol used by browsers and web services. A method, path, headers, and optional body describe a request.
Session
State kept across related requests, often to track a signed-in user. Authentication and permission checks remain separate concerns.
Container
An isolated process environment created from an image. Persistent data needs storage outside the container’s disposable filesystem.
Docker Compose
A configuration format and tool for starting related containers, networks, and volumes. Startup order alone does not guarantee service readiness.
Volume and bind mount
A volume is storage managed by the container platform; a bind mount exposes a chosen host path inside a container.
Published port
A host port mapped to a service’s container port. Publishing a port can expose that service beyond its container network.
CLI
A text interface in which commands and arguments select operations. Exit codes let scripts detect success or failure.
Build
The steps that turn source files and dependencies into a runnable program or distributable artifact.
Continuous integration
Automated checks that run when source changes, such as building, testing, and linting. Passing checks cover only what those checks exercise.
Mermaid
A text-based diagram language. Nodes and connections describe a model; the diagram itself does not execute the project.
Backend
An implementation behind a shared interface. Depending on context, it can mean a server-side service or a graphics, input, or storage implementation.
Database
A system for storing and querying structured records. Its schema, access rules, and persistence determine how the application can use that data.
15. References
- antoinebou12. (2026, November 5). TCH099. [Software repository] github.com.
- antoinebou12. (2026, November 5). TCH099: README. [Repository documentation] github.com.
- antoinebou12. (2026, November 5). routes.php. [Source file] github.com.
- antoinebou12. (2026, November 5). router.php. [Source file] github.com.
- antoinebou12. (2026, November 5). README.md. [Source file] github.com.
- Boucher, A. (2026, October 23). TCH099: One PHP router for pages and API calls. [Project article] antoineboucher.info.
Sources without a publication date show the date they were last checked, November 5, 2026. File links retain the inspected revision where available.