Running locally

The recommended way to run a site while you develop is Docker: one container with Apache, PHP and MySQL, with the framework modules and your site mounted in as volumes. Your edits are live on the next page refresh, and the whole setup is a single file you can commit alongside the site.

What you need

Docker installed on your machine, and a few folders on disk:

  • the framework modules, the shared set of framework modules,
  • your site, a folder with a public_html/index.php entry point and a Config.php beside it,
  • an empty data folder, where MySQL keeps the database files so they stay on the host between runs.

The Site structure page covers what goes in the site folder. This page gets that folder served and talking to a database.

The Dockerfile

Save this as Dockerfile. It installs Apache, PHP and MySQL, points the web root at your site's public_html, sends every unmatched request to index.php so the framework can route it, and creates the database the site uses. It also initialises MySQL's data directory on first start, so the database files can live on a folder mounted from the host and survive the container being rebuilt or removed.

FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive

# Apache, PHP 8.3 and MySQL
RUN apt-get update
RUN apt-get install -y software-properties-common
RUN add-apt-repository -y ppa:ondrej/php
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y apache2 php8.3 libapache2-mod-php8.3 php8.3-mysql php8.3-cli php8.3-common curl unzip
RUN apt-get install -y mysql-server
RUN apt-get clean

# Where the framework and the site are mounted (see docker run below)
RUN mkdir -p /var/www/framework /var/www/site

# Apache virtual host: the site's public_html is the web root, and every
# unmatched request is handed to index.php, which does the framework's routing.
RUN echo "<VirtualHost *:8080>\n"\
"ServerName localhost\n"\
"DocumentRoot /var/www/site/public_html\n"\
"ErrorDocument 403 /index.php\n"\
"ErrorDocument 404 /index.php\n"\
"<Directory /var/www/site/public_html>\n"\
"Options FollowSymLinks\n"\
"AllowOverride All\n"\
"Require all granted\n"\
"</Directory>\n"\
"</VirtualHost>" > /etc/apache2/sites-available/site.conf

RUN a2ensite site
RUN a2dissite 000-default.conf
RUN a2enmod rewrite
RUN echo "Listen 8080" >> /etc/apache2/ports.conf

# Create the application database and a user for it. IF NOT EXISTS keeps this
# safe to run on every start, once the data directory persists on the host.
RUN echo "CREATE DATABASE IF NOT EXISTS app_db;\n"\
"CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'password';\n"\
"GRANT ALL PRIVILEGES ON app_db.* TO 'app'@'%';\n"\
"FLUSH PRIVILEGES;" > /root/database.sql

# Show PHP errors while developing
RUN sed -i "s/display_errors = .*/display_errors = On/" /etc/php/8.3/apache2/php.ini
RUN sed -i "s/error_reporting = .*/error_reporting = E_ALL/" /etc/php/8.3/apache2/php.ini

# Bind MySQL so the app can reach it, and allow native-password auth
RUN sed -i "s/bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/mysql.conf.d/mysqld.cnf

# The MySQL data directory is a host volume (see docker run), so the first time
# it is empty. Initialise it if needed and make sure MySQL owns it, then start
# MySQL, load the database, and run Apache in the foreground.
RUN echo "#!/bin/sh\n"\
"if [ ! -d /var/lib/mysql/mysql ]; then\n"\
"  mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql\n"\
"fi\n"\
"chown -R mysql:mysql /var/lib/mysql\n"\
"service mysql start\n"\
"sleep 3\n"\
"mysql -u root < /root/database.sql\n"\
"apachectl -D FOREGROUND" > /root/entrypoint.sh
RUN chmod +x /root/entrypoint.sh

EXPOSE 8080
CMD ["/root/entrypoint.sh"]

Build and run

Build the image once:

docker build -t phpframework .

Then run it, mounting three folders as volumes: the framework and the site so your edits are live, and a data folder for MySQL so the database is stored on the host rather than inside the container. Point each path at a folder on your machine; the data folder can start empty and the container fills it on first run:

docker run --detach -p 8080:8080 \
  -v /path/to/framework:/var/www/framework \
  -v /path/to/site:/var/www/site \
  -v /path/to/data:/var/lib/mysql \
  --name phpframework_dev phpframework

Open http://localhost:8080. The site is served from /var/www/site/public_html, and because the folders are volumes, editing a file on your machine changes what the container serves on the next refresh. No rebuild is needed for code changes.

Because the database lives in the mounted data folder, it survives the container being stopped, removed or rebuilt: your records are still there next time you start it. Delete that folder when you want a clean database from scratch.

How the request reaches your code

The virtual host sets the web root to your site's public_html and sends both 404 and 403 responses to /index.php. That single entry point is the front controller: every request that is not a real file on disk arrives at index.php, which includes the modules and lets the framework's routing decide what to render. This is why a site has just one PHP file in its web root and everything else lives in modules.

The first request

On the first request, each module installs its own database tables through its migration scripts, and the admin account you configured is seeded. Later requests skip the install step because the framework records which version each module has reached. If the page cannot reach the database, check that the site's Config.php points at host 127.0.0.1, database app_db and user app, matching the Dockerfile above.

Next: Site structure.