MVC Comparison
This page explains the differences between traditional MVC frameworks and Phexium's Clean Architecture with CQRS. It is intended for developers transitioning from Laravel, Symfony, Rails, or similar MVC frameworks.
Traditional MVC
The Model-View-Controller pattern organizes code into three components:
Request → Controller → Model → View → Response
│ │
│ └─ ORM + Business Logic + Validation
└─ HTTP + Validation + Business Logic + Response
Component Responsibilities
| Component | Typical Responsibilities |
|---|---|
| Controller | HTTP handling, validation, business logic orchestration, response formatting |
| Model | ORM/Active Record, business rules, data validation, persistence |
| View | Template rendering, display logic |
Limitations
- Coupling: Controllers often contain business logic mixed with HTTP concerns
- Testability: Models depend on database, controllers on HTTP context
- Scalability: Large controllers and "fat models" become difficult to maintain
Phexium Architecture
Phexium separates concerns into distinct layers with explicit responsibilities:
Request → Controller → Command → Bus → Handler → Entity
│ │ │
│ │ └─ Business Rules
│ └─ Orchestration
└─ HTTP only
Entity → Presenter → ViewModel → Template → Response
Layer Responsibilities
| Layer | Responsibility |
|---|---|
| Controller | HTTP parsing, command/query creation, response handling |
| Command/Query | Immutable data transfer object |
| Handler | Use case orchestration, no business logic |
| Entity | Business rules, invariants, state changes |
| Presenter | Data transformation for display |
| ViewModel | Display-ready data structure |
Terminology Mapping
| MVC Concept | Phexium Equivalent | Key Difference |
|---|---|---|
| Controller | Controller + Command + Handler | Orchestration separated from logic |
| Model | Entity + Repository + Value Object | Domain isolated from persistence |
| View | Presenter + ViewModel + Template | Explicit transformation layer |
| - | Bus (Command/Query) | Dispatch with middleware support |
| - | Domain Events | Decoupled cross-cutting communication |
| Active Record | Repository Interface | Persistence abstracted behind interface |
| Model validation | Value Object | Validation at construction time |
Comparison by Responsibility
HTTP Request Handling
In MVC, controllers handle HTTP concerns and business logic together:
// MVC Controller
public function store(Request $request)
{
$validated = $request->validate(['title' => 'required']);
$book = Book::create($validated);
return redirect('/books');
}
In Phexium, the controller delegates to a handler through a command:
// Phexium Controller
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$data = (array) $request->getParsedBody();
$command = new CreateBookCommand(Uuid::uuid4()->toString(), $data['title'] ?? '');
$this->commandBus->dispatch($command);
return $response->withHeader('Location', '/books')->withStatus(302);
}
Business Logic
In MVC, business logic resides in the Model or Controller:
// MVC Model
class Book extends Model
{
public function borrow(User $user)
{
if ($this->status !== 'available') {
throw new Exception('Book not available');
}
$this->status = 'borrowed';
$this->save();
}
}
In Phexium, business logic belongs to the Entity, invoked by a Handler:
// Phexium Entity
public function markAsBorrowed(): void
{
if (!$this->status->canBeBorrowed()) {
throw BookNotAvailableException::withId($this->id);
}
$this->status = BookStatus::Borrowed;
}
Data Access
MVC uses Active Record or a tightly coupled ORM:
Phexium uses Repository interfaces defined in the Domain, implemented in Infrastructure:
// Domain Interface
interface BookRepositoryInterface
{
public function findById(IdInterface $id): ?Book;
}
// Infrastructure Implementation
final class SqliteBookRepository implements BookRepositoryInterface
{
public function findById(IdInterface $id): ?Book { /* ... */ }
}
Side-by-Side Comparison
┌─────────────────────────────────────────────────────────────────┐
│ MVC Pattern │
├─────────────────────────────────────────────────────────────────┤
│ Request → Controller → Model → View → Response │
│ │ │ │
│ │ └─ ORM + Business Logic + Validation │
│ └─ HTTP + Validation + Business Logic + Response │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Clean Architecture + CQRS │
├─────────────────────────────────────────────────────────────────┤
│ Request → Controller → Command → Bus → Handler → Entity │
│ │ │ │ │
│ │ │ └─ Rules │
│ │ └─ Orchestration │
│ └─ HTTP only │
│ │
│ Entity → Presenter → ViewModel → Template → Response │
└─────────────────────────────────────────────────────────────────┘
Benefits of the Transition
Testability
Each layer can be tested in isolation:
- Entities: Pure unit tests without database or HTTP
- Handlers: In-memory repositories, no external dependencies
- Controllers: Mock the bus, test HTTP behavior only
- Presenters: Input/output transformation tests
Maintainability
Responsibilities are explicit:
- Adding a feature means adding a new Command + Handler
- Existing code remains unchanged (Open/Closed Principle)
- Business rules are centralized in Entities
Flexibility
Infrastructure can be replaced without touching business logic:
- Switch database (SQLite → PostgreSQL): only repository implementation changes
- Change UI framework: only Presentation layer changes
- Add caching: middleware on the bus, no handler changes
See Also
- Clean Architecture - Layer separation and dependency rules
- CQRS - Command Query Responsibility Segregation
- Controllers - Controller implementation details
- Entities - Domain entity patterns
- Presenters - Data transformation for views