Case study

CorianderPHP

A personal R&D project for understanding how a PHP framework works internally, from routing to tests through CLI tooling and automation.

Screenshot of the CorianderPHP documentation website.

Project resources

The framework and its documentation are maintained in two separate repositories. The documentation has its own build, tests, and automated update workflow.

Introduction

I started CorianderPHP because I wanted to better understand what was happening behind the framework APIs I was already using. Instead of only learning how to use Laravel or other tools, I wanted to implement some of their mechanisms and understand how they worked.

The goal was not to recreate Laravel or Symfony. I wanted a framework small enough to understand from end to end, while keeping a clear structure for my projects.

This approach also gives me a lightweight base for some personal projects, when the features of a more complete framework are not necessary.

Why I built it

CorianderPHP quickly became a research and learning project. Each new feature is an opportunity to look at how other frameworks solve the same problem, then understand the reasons behind their choices.

The project led me to work on routing, the request/response cycle, middleware, dependency injection, tests, Composer, PSR standards, GitHub Actions, and CI/CD.

Implementing these mechanisms myself also helped me better understand the role of abstractions. They can simplify a project when they answer a real need, but they can also add unnecessary complexity when they are introduced too early.

Static and dynamic views

One of the first subjects I wanted to simplify was view routing. For a simple page, CorianderPHP can determine the URL directly from its place in the folder tree. When a page needs parameters or prepared data, an explicit route and controller take over.

Unlike some routing systems based entirely on files, dynamic parameters are not created with files like [id].php. I prefer declaring these routes explicitly and using a controller so data preparation stays clear.

Static view

The URL can come directly from the folders inside public/public_views.

public
public_views
about
index.php
metadata.php
Generated URL /about
Static view documentation

Dynamic view

A route calls a controller, the controller prepares data, and a normal view renders the result.

1

Request

The browser opens /articles/42.

2

Route

public/routes.php matches /articles/{id}.

It calls ArticleController::show().

3

Controller

The controller reads the route id, loads the article, and prepares view data.

4

View

public/public_views/articles/show/index.php renders the prepared variables.

Dynamic view documentation

Architecture and framework internals

Working on CorianderPHP changed the way I look at the frameworks I use. Many mechanisms feel automatic when you only work with their APIs. Implementing them helps me better understand each component's responsibilities and where some abstractions come from.

I worked directly with the HTTP request and response lifecycle, middleware, controllers, views, dependency injection, modules, Composer, and PSR interfaces like PSR-7, PSR-15, and PSR-3.

This experience helped me better understand how these different elements interact while a request is being processed.

CLI and developer experience

I also developed a CLI to group common operations around the framework. The goal is to make some repetitive tasks easier without hiding what happens behind the commands.

It can generate files, run front-end builds, manage cache, configure the database, run migrations, check the installed version, and update the framework.

Generation

Views, controllers, routes, modules, API controllers, and migrations.

Assets

Run TypeScript and Tailwind tasks from the project root.

Database

Connection configuration, PDO usage, and migration management.

Migrations

Batch tracking, status checks, rollback, and detection of migrations modified after execution.

Framework update

Preview updates, protect local changes, create backups, and rollback if something fails.

The update system was especially interesting to design. It led me to work on versioning, release archives, local change detection, backups, and rollback mechanisms.

The database part raised a similar question: how far should usage be simplified without completely hiding SQL or making the framework behavior hard to understand?

Keeping it small

CorianderPHP deliberately stays limited in scope. I do not add a feature simply because it exists in other frameworks: it has to answer an identified need and have a clear place in the project.

One of the useful exercises with this project is deciding what not to build.

More complete frameworks are suited to many projects and teams. CorianderPHP simply gives me a smaller environment to experiment, understand architecture choices, and build my own projects.

CI with GitHub Actions

CorianderPHP also helped me go deeper with GitHub Actions. I wanted to automate quality checks and make sure each change goes through the same validation process.

1

Trigger

Push or pull request targeting main.

2

Validate and install

Composer files are checked, then dependencies are cached and installed.

3

Audit

Dependencies are checked for known security issues.

4

Lint

PHP files are checked for syntax errors.

5

Test

The PHPUnit suite runs.

This workflow lets me work with CI in practice and secure the development cycle without relying only on checks run locally.

Automated releases and documentation updates

I also wanted to experiment with automation between several repositories. Publishing a new framework version therefore triggers an update chain on the documentation side.

1

Framework release

A new framework version is published.

2

Collect release information

The workflow gathers tags, commits and changed files.

3

Dispatch to documentation repository

Release context is sent to the documentation repository.

4

Update framework in documentation project

The documentation project updates the framework.

5

Build assets and regenerate downloads

Assets and guided project downloads are regenerated.

6

Run tests

The documentation test suite runs.

7

Create Pull Request

The update is prepared for review.

8

Manual review

I review the Pull Request before merging.

The two repositories communicate through a repository_dispatch event containing information about the release and the changes since the previous version.

I deliberately kept a human validation step. The automation handles repetitive operations and prepares the update, but the documentation is not changed automatically without review.

Documentation as part of the project

I consider documentation a full part of the project. It has its own validation cycle and does not depend only on a manual update after each framework change.

Its workflow installs PHP and Node.js dependencies, rebuilds front-end assets, regenerates downloadable projects, and runs the test suite.

This organization also lets framework releases automatically prepare the next documentation updates.

What I learned

The main value of CorianderPHP is what the project teaches me while I build it. I now understand much better how a request moves through a framework, how responsibilities can be separated, and why some abstractions exist.

The project also taught me to pay more attention to complexity. An abstraction can make a codebase clearer when it solves a real problem. Introduced too early, it can instead make a small project harder to understand and evolve.

CorianderPHP remains an experimental project and continues to serve as a research ground. I use it to test ideas around architecture, testing, tooling, and automation. The framework is useful for my own projects, but its main value remains everything its design lets me learn.