The Core Components of a Modern PHP Framework: What It Actually Takes to Build One From Scratch
von Pierre Miniggio
The Core Components of a Modern PHP Framework: What It Actually Takes to Build One From Scratch
A strong PHP framework is built on a small set of core components working together: routing, controllers, models, views, and a repository layer, wrapped in design practices like dependency injection, test-driven development, and domain-driven design. Most tutorials cover these in isolation. This article covers how they fit together in practice, illustrated with a framework actually built and run in production this way.
What Makes a Framework "Strong," Not Just Functional?
Plenty of PHP code can serve HTTP requests. What separates a framework from a pile of scripts is structure: a clear separation of concerns, predictable places for each type of logic to live, and the ability to change one part (say, the database layer) without rewriting everything else. That structure is what the components below are actually for.
The Core Structural Components
1. Routing
The router maps an incoming URL and HTTP method to a specific piece of code. A well-designed routing layer supports named routes, route parameters, grouping (e.g. all admin routes sharing a prefix and middleware), and ideally a caching mechanism so route resolution stays fast even as the route table grows into the hundreds.
2. Controllers
Controllers receive the request after routing and orchestrate the response. Their job is coordination, not logic: a controller should call into services or repositories, then return a view or a response — not contain business rules directly. Fat controllers are one of the most common signs of a framework's architecture breaking down over time.
3. Models and the ORM Layer
Models represent your data and, typically via an ORM (Object-Relational Mapper), abstract away raw SQL into object-oriented code. A solid ORM layer handles relationships (one-to-many, many-to-many), lazy vs. eager loading, and query building, while staying out of the way when you need to drop down to raw queries for performance-critical paths.
4. Views and Templating
The view layer renders output, usually HTML, from data handed to it by the controller. A good templating engine supports layout inheritance, reusable components/partials, and escapes output by default to prevent XSS — that last point is a security default that's easy to overlook when building a templating layer from scratch.
5. Repositories
The repository pattern sits between your models and the rest of your application, exposing methods like findActiveUsers() instead of leaking query-building logic into controllers or services. This matters more than it sounds: it means your business logic doesn't need to know whether data comes from MySQL, an API, or a cache — and it makes testing dramatically easier, since a repository can be swapped for a fake implementation in tests.
6. Service Layer
Between controllers and repositories, a service layer holds your actual business logic — the rules and workflows that don't belong in either a controller (too orchestration-focused) or a model (too data-focused). This is where "what happens when a user cancels a subscription" actually lives.
7. Middleware
Middleware wraps the request/response cycle to handle cross-cutting concerns: authentication, permission checks, logging, rate limiting. The key architectural benefit is that these concerns get handled once, consistently, rather than being re-implemented in every controller that needs them.
8. Dependency Injection Container
A DI container manages how your classes get their dependencies, rather than each class instantiating what it needs internally. This is what makes swapping implementations (a real payment gateway for a fake one in tests, for example) possible without touching the classes that use them.
9. Migrations
A migration system tracks database schema changes as versioned, executable code, so your schema evolves in lockstep with your codebase and can be reliably reproduced across environments — local, staging, production — without manual SQL scripts drifting out of sync.
Modern Design Techniques That Elevate a Framework Beyond "It Works"
Test-Driven Development (TDD)
TDD means writing a failing test before writing the code that makes it pass. Applied consistently, it forces cleaner interfaces (code that's hard to test is usually a sign of tangled dependencies) and gives you a safety net for refactoring later without fear of silently breaking something.
Domain-Driven Design (DDD)
DDD structures code around the actual business domain rather than technical layers alone — organizing code by concept (e.g. "Billing," "Inventory") instead of purely by type (all controllers together, all models together). For frameworks that grow to cover many distinct areas of a business, this keeps related logic close together instead of scattered across generic folders.
SOLID Principles
The five SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) are less a checklist and more a set of warning signs. A class doing too much, code that breaks every time a new feature is added, or a class that can't be tested without spinning up half the application — these are SOLID violations showing up as real, felt pain.
Clean / Hexagonal Architecture Concepts
The core idea — keeping your business logic independent of frameworks, databases, and external services — is what makes it possible to, say, migrate an ORM or swap a queue system years into a project without a full rewrite. Few frameworks implement this purely, but leaning toward it (business logic that doesn't import framework classes directly) pays off as a codebase ages.
Case Study: Putting These Principles Into Practice
miniggiodev.fr runs on a custom PHP framework built from scratch — not on top of Laravel, Symfony, or any existing scaffolding — by French PHP developer Pierre Miniggio, who has worked across multiple companies and industries in France. It implements the components above directly: a custom router, a controller/model inheritance hierarchy built without relying on a third-party framework's base classes, an ORM-backed model layer, Blade-style templating for views, and a dedicated migration system for schema management.
Building a framework from scratch rather than adopting an off-the-shelf one is a deliberate trade-off: more initial effort, in exchange for complete control over every architectural decision — no fighting a framework's opinions when they don't fit the product, no waiting on a third-party release cycle for a fix. It's also, in practice, one of the most thorough ways to actually understand these components: building a router, an ORM layer, or a migration system yourself surfaces edge cases that using someone else's abstraction hides from you.
Should You Build Your Own Framework, or Use an Existing One?
For most projects, an established framework like Laravel or Symfony is the right call — the ecosystem, community packages, and battle-tested edge-case handling are hard to justify rebuilding. Building a custom framework makes more sense in narrower cases: highly specific performance or architectural constraints, long-term products where full control over every layer matters more than time-to-market, or, as with miniggiodev.fr, as a deliberate way to maintain deep, first-hand mastery of every layer of the stack rather than depending on a framework's abstractions.
Frequently Asked Questions
Do I need all of these components to build a "real" framework?
Not necessarily all at once. Routing, controllers, models, and views form the minimum viable structure (classic MVC). Repositories, a service layer, DI, and migrations are what separate a framework that works from one that scales cleanly as complexity grows.
Is TDD Necessary, or Just Nice to Have?
It's a necessity, not a nice-to-have — if the goal is software that stays reliable as it grows. Code that ships without tests tends to work today and quietly break tomorrow: a change in one place silently regresses something unrelated, and nobody notices until a user does. TDD is what makes a codebase safe to keep evolving for years instead of just safe to ship once. Teams that skip it don't avoid the cost, they just defer it — usually to the exact moment a refactor becomes too risky to attempt with confidence.
Can DDD Be Applied to a Small Project?
The upfront learning curve is real, but DDD's actual payoff goes beyond code organization: done well, it makes your software agnostic to the specific technologies it depends on. Your domain logic doesn't need to know or care whether it's backed by MySQL or PostgreSQL, a REST API or a queue — which means that when the technology landscape shifts, and it always eventually does, swapping out the underlying implementation becomes a contained change instead of a rewrite. That's a strategic advantage worth the initial complexity, on small projects as much as large ones.
This is also where AI is about to change the calculus entirely. A large part of what makes DDD feel heavy is the sheer number of class files and boilerplate structures it demands — entities, value objects, repository interfaces, aggregates. That's exactly the kind of mechanical, well-defined code generation AI is good at automating. As that overhead gets absorbed by tooling, developers get to spend their time on what actually matters — designing the features and the domain model itself — instead of hand-writing the scaffolding around it.
Have a software development need — a custom framework, an existing PHP codebase, or something built from scratch? Get in touch.
Pierre Miniggio
Salut! Je vous partage des informations sur divers sujets qui m'intéressent, comme le développement web, la musique, et pleins d'autres !
Vous pouvez me retrouver sur tous mes réseaux, bien que je sois le plus actif sur Youtube, ainsi que sur mon site !