Skip to content
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.

Web appAPIDocker
PHP 8MariaDBPDOnginxApacheAzure App ServiceGitHub ActionsAndroidOkHttp
  • Updated

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.php lists every route as a call such as get('/login', 'frontend/pages/login.html') or post('/api/login', 'api/login.php'). router.php defines 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 with fetch.
  • API. api/login.php, signup.php, logout.php, user_details.php, clients.php, hello-world.php and random-image.php read JSON bodies and answer with JSON. Shared helpers (loginUser, registerUser, isAdmin, CSRF helpers) live in api/utils/utils.php.
  • Data. config.php opens a PDO connection to MySQL/MariaDB. db/1create.sql creates Clients (id, unique username and email, password hash, role ENUM('admin','client'), timestamps); db/db.py runs the three SQL files with credentials from .env.
  • Mobile. MyMobileApp/ is an Android Studio project with login, sign-up and user-details activities. HttpClient.java keeps 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.

Use-case diagram for TCH099: Create an account; Log in with a session; Show a random image; Get a greeting by language; List clients as adminTCH099Create an accountLog in with a sessionShow a random imageGet a greeting by languageList clients as adminWeb or mobile userAdministrator«system»MariaDB database
People on the left, external systems on the right; each oval is a use case backed by the files cited below.
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

  1. Fill the DB_HOST and MYSQL_* variables listed in the README, then run docker-compose up -d --build.
  2. 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.
  3. Open http://localhost:8001/signup, create an account, then log in at /login.
  4. With an admin account, open /admin to see the Clients table loaded from /api/clients.
  5. 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:

  1. 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.
  2. Project structure. An annotated tree of api/, db/, frontend/, the router files and the deployment files.
  3. Database. The .env variables and the db.py initialisation commands.
  4. API. Endpoint, method, request body and response for each route, with a PlantUML sequence diagram per call.
  5. Front end. A DevTools tour (Elements, Console, Sources, Network, Performance, Memory, Application, Security) with screenshots and shortcuts.
  6. Mobile. Android Studio setup, the buildConfigField API URLs, the layout files, and why the emulator reaches the host at 10.0.2.2.
  7. 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:

  1. If the target is a string without .php or .html, it appends .php.
  2. It cleans the request URI, strips a trailing slash and the query string, and splits both the route and the URI on /.
  3. A route ending in * matches any longer path and passes the rest as one argument.
  4. 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.
  5. 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. api and hello-world match, and $langue captures es.
  • hello-world.php is included inside route(), so it sees $langue = 'es', reads nom and prenom from the body (or uses the logged-in username as prenom), 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.

Azure portal Deployment Center for the tch099antoine Web App, linked to the GitHub repository’s main branch with GitHub Actions as build provider and PHP 8.2
Azure Deployment Center from the repository’s guide: source GitHub, branch main, build provider GitHub Actions, PHP 8.2.

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.php hard-codes the local credentials (tch099-db, user/password) and never reads the environment; $isCloudEnv is computed and unused. The Azure guide works around this by writing a .env over SSH, but PHP does not load it.
  • config.php echoes “Database connection successful.” on every include. API responses therefore start with that text before the JSON, which makes response.json() in the pages reject.
  • api/signup.php takes role from the request body, so anyone can create an admin account and then open /admin and /api/clients.
  • api/clients.php returns SELECT * FROM Clients, password hashes included.
  • router.php sets Access-Control-Allow-Origin: * and turns on display_errors for every request.
  • The README’s “add a route” section shows a $routes['/path'] = [...] array format that routes.php does not use; the real format is the get() / post() calls above.
  • The CSRF helpers in utils.php are not called by any API script.

10. Sources

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
Workflows, triggers, jobs and targets
WorkflowTriggersJobsPublishes to
Build and deploy PHP app to Azure Web App - tch099antoine
.github/workflows/main_tch099antoine.yml
push, manual run
  • build ubuntu-latest
    actions/checkout · Setup PHP · Check if composer.json exists · Run composer install if composer.json exists · Zip artifact for deployment · Upload artifact for deployment job
  • deploy ← build ubuntu-latest
    Download artifact from build job · Unzip artifact for deployment · Login to Azure · Deploy to Azure Web App
—
Deploy static content to Pages
.github/workflows/static.yml
Runs, on, pushes, targeting, the, default
  • deploy ubuntu-latest
    Checkout · Setup Pages · Upload artifact · Deploy to GitHub Pages
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
External dependencies
PackageVersionScopePurposeRegistry
appcompat—runtime—JVM (Gradle)
material—runtime—JVM (Gradle)
activity—runtime—JVM (Gradle)
constraintlayout—runtime—JVM (Gradle)
okhttp—runtimeHTTP clientJVM (Gradle)
okio—runtime—JVM (Gradle)
dotenv—runtimeConfigurationJVM (Gradle)
loggingInterceptor—runtimeLogging and observabilityJVM (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

  1. antoinebou12. (2026, November 5). TCH099. [Software repository] github.com.
  2. antoinebou12. (2026, November 5). TCH099: README. [Repository documentation] github.com.
  3. antoinebou12. (2026, November 5). routes.php. [Source file] github.com.
  4. antoinebou12. (2026, November 5). router.php. [Source file] github.com.
  5. antoinebou12. (2026, November 5). README.md. [Source file] github.com.
  6. 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.