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.

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.
/about
Dynamic view
A route calls a controller, the controller prepares data, and a normal view renders the result.
Request
The browser opens /articles/42.
Route
public/routes.php matches /articles/{id}.
It calls ArticleController::show().
Controller
The controller reads the route id, loads the article, and prepares view data.
View
public/public_views/articles/show/index.php renders the prepared variables.
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.
Trigger
Push or pull request targeting main.
Validate and install
Composer files are checked, then dependencies are cached and installed.
Audit
Dependencies are checked for known security issues.
Lint
PHP files are checked for syntax errors.
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.
Framework release
A new framework version is published.
Collect release information
The workflow gathers tags, commits and changed files.
Dispatch to documentation repository
Release context is sent to the documentation repository.
Update framework in documentation project
The documentation project updates the framework.
Build assets and regenerate downloads
Assets and guided project downloads are regenerated.
Run tests
The documentation test suite runs.
Create Pull Request
The update is prepared for review.
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.