Skip to content

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:

// MVC - Active Record
$book = Book::find($id);
$books = Book::where('status', 'available')->get();

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