Question

How to Structure the Model in MVC with PHP

phpoopmodel-view-controllerarchitecturemodel

Question

I am learning the MVC pattern and I am unsure how much code should belong in the model layer.

I currently use a data access class with methods like this:

public function checkUsername($connection, $username)
{
    try {
        $data = [];
        $data['Username'] = $username;

        $sql = "SELECT Username FROM " . $this->usersTableName . " WHERE Username = :Username";

        return $this->executeObject($connection, $sql, $data);
    } catch (Exception $e) {
        throw $e;
    }
}

My models are usually entity classes mapped to database tables.

Should a model contain both:

  • the database-mapped properties, and
  • methods like the database access code above

Or is it acceptable to separate the database access code into another class that performs the actual database work?

If I separate these responsibilities, does that effectively mean I now have four layers instead of three in MVC?

Short Answer

By the end of this page, you will understand what the Model means in MVC, how it differs from a plain database entity, and why many PHP applications separate domain objects, business logic, and data access into different classes. You will also see why adding repository or service classes does not break MVC—it usually makes the code easier to test, maintain, and extend.

Concept

In MVC, the Model is not just a single class and it is not only a database row. The model represents the data and business rules of the application.

For beginners, this is the key idea:

  • View: shows data to the user
  • Controller: handles requests and coordinates work
  • Model: contains application data, rules, and operations related to that data

A common source of confusion is that many tutorials treat the model as either:

  • a class that maps directly to a database table, or
  • a class that only runs SQL queries

In real projects, the model layer is often made of multiple parts:

  • Entity / domain object: represents data like User
  • Repository / data access object: loads and saves data from the database
  • Service / business logic class: performs rules such as registration, validation, or permission checks

That still fits MVC. These classes are all part of the model layer because they represent the application's data and logic, not the user interface.

Your checkUsername() example is really about data access. It asks the database whether a username exists. That logic often belongs in a repository or data access class, not directly inside a plain entity object.

Why this matters:

  • It keeps entities simple and focused
  • It prevents business objects from being tightly coupled to SQL
  • It makes testing easier
  • It allows you to change the database code without rewriting the rest of the application

So yes, separating database work from the object that represents the data is a common and healthy design choice.

Mental Model

Think of MVC like running a library:

  • The View is the front desk display that shows information to visitors.
  • The Controller is the librarian taking requests like "find this book".
  • The Model is everything about the library's information and rules.

Inside the model layer, you may have different roles:

  • An entity is the book card: title, author, ISBN
  • A repository is the storage clerk who knows where and how to fetch books
  • A service is the policy expert who knows the rules, like who may borrow a rare book

Even though there are multiple roles, they all belong to the library's internal system. That does not mean the library stopped being a library. In the same way, adding classes inside the model layer does not mean MVC has become something else.

Take Quiz

Syntax and Examples

In PHP, a clean MVC-style structure often separates the parts of the model layer.

1. Entity

An entity holds the data for a user.

class User
{
    public int $id;
    public string $username;
    public string $email;

    public function __construct(int $id, string $username, string $email)
    {
        $this->id = $id;
        $this->username = $username;
        $this->email = $email;
    }
}

This class represents a user. It does not need to know SQL.

2. Repository

A repository handles database queries.

class UserRepository
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function usernameExists(string $username): bool
    {
        $sql = "SELECT 1 FROM users WHERE username = :username LIMIT 1";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['username' => $username]);

        return (bool) $stmt->fetchColumn();
    }
}

This class is responsible for talking to the database.

3. Service

A service can apply business rules.

class UserService
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function canRegisterUsername(string $username): bool
    {
        if (trim($username) === '') {
            return false;
        }

        return !$this->userRepository->usernameExists($username);
    }
}

This class answers a business question: can this username be used?

4. Controller

The controller coordinates the request.

class UserController
{
    private UserService $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function checkUsernameAction(string $username): void
    {
        $available = $this->userService->canRegisterUsername($username);

        echo $available ? 'Available' : 'Already taken';
    }
}

Why this is better

  • User represents data
  • UserRepository handles SQL
  • UserService handles rules
  • UserController handles the web request

That separation makes each class easier to understand.

Step by Step Execution

Consider this example:

$repository = new UserRepository($pdo);
$service = new UserService($repository);
$controller = new UserController($service);

$controller->checkUsernameAction('alice');

Now trace what happens:

Step 1: Create the repository

$repository = new UserRepository($pdo);
  • The repository receives a PDO database connection.
  • It is now ready to run SQL queries.

Step 2: Create the service

$service = new UserService($repository);
  • The service receives the repository.
  • It can now ask whether usernames exist.

Step 3: Create the controller

$controller = new UserController($service);
  • The controller receives the service.
  • It does not need to know SQL details.

Step 4: Handle the action

$controller->checkUsernameAction('alice');
  • The controller calls canRegisterUsername('alice').
  • The service trims the input and checks whether it is empty.
  • If not empty, the service asks the repository whether alice already exists.
  • The repository runs this query:
SELECT 1 FROM users WHERE username = :username LIMIT 1
  • If the query finds a row, usernameExists() returns true.
  • The service negates that result and returns whether the username is available.
  • The controller prints either Available or Already taken.

Important observation

Each class does one clear job. This is easier to debug than putting SQL, validation, and output logic in one place.

Real World Use Cases

Here are common places where this structure helps.

User registration

  • Entity: User
  • Repository: find existing users, save new users
  • Service: check username uniqueness, hash passwords, enforce rules

E-commerce

  • Entity: Order, Product, Customer
  • Repository: load orders and products from the database
  • Service: calculate totals, apply discounts, validate stock

Blog or CMS

  • Entity: Post, Comment
  • Repository: fetch posts by slug or author
  • Service: publish/unpublish posts, validate content, moderate comments

APIs

  • Repository: read and write records
  • Service: apply validation and business rules before returning JSON
  • Controller: convert request input into service calls and responses

Reporting or data processing

  • Repository: fetch raw records
  • Service: group, transform, or summarize data
  • View/API response: present the final result

In all these cases, putting everything directly into one model class quickly becomes hard to manage.

Take Quiz

Real Codebase Usage

In real PHP codebases, developers often treat the model layer as a group of related classes, not a single file.

Common patterns

Guard clauses

Developers reject invalid input early.

public function canRegisterUsername(string $username): bool
{
    if (trim($username) === '') {
        return false;
    }

    return !$this->userRepository->usernameExists($username);
}

This avoids deeply nested if statements.

Repositories for persistence

Database logic is often grouped in repository classes.

$user = $userRepository->findById(10);
$userRepository->save($user);

This keeps SQL in one place.

Services for business logic

A service may coordinate several repositories.

$orderService->placeOrder($customerId, $items);

Inside that method, it might:

  • validate items
  • check stock
  • create an order
  • save records
  • send notifications

Validation before persistence

Controllers should not contain all business rules.

Instead of this:

if (strlen($username) < 3) {
    echo 'Invalid';
}

developers often move such rules into services or domain logic.

Error handling

Repositories may throw exceptions when the database fails. Services may catch and translate them into application-level errors.

Practical takeaway

A real codebase usually grows beyond one Model class. That is normal. MVC defines responsibilities at a high level; it does not force every responsibility into one class.

Common Mistakes

Beginners often confuse model with database table class. Here are common mistakes.

1. Putting SQL directly inside the controller

Broken example:

class UserController
{
    public function checkUsernameAction(PDO $pdo, string $username): void
    {
        $stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = :username");
        $stmt->execute(['username' => $username]);

        echo $stmt->fetchColumn() ? 'Taken' : 'Available';
    }
}

Why this is a problem:

  • The controller now knows database details
  • It becomes hard to test
  • Reusing the logic elsewhere is harder

Better: move database work to a repository.

2. Making an entity responsible for database access

Broken example:

class User
{
    public string $username;

    public function usernameExists(PDO $pdo): bool
    {
        $stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = :username");
        $stmt->execute(['username' => $this->username]);

        return (bool) $stmt->fetchColumn();
    }
}

Why this is risky:

  • The object mixes data representation with persistence
  • It couples the entity tightly to the database

3. Catching and rethrowing the same exception unnecessarily

Your original code did this:

try {
    // code
} catch (Exception $e) {
    throw $e;
}

This usually adds no value. If you are not adding context or handling the error, let it bubble up naturally.

Better:

public function usernameExists(string $username): bool
{
    $sql = "SELECT 1 FROM users WHERE username = :username LIMIT 1";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute(['username' => $username]);

    return (bool) $stmt->fetchColumn();
}

4. Passing the database connection into every method

Less ideal:

public function checkUsername($connection, $username)

Better:

public function __construct(PDO $pdo)
{
    $this->pdo = $pdo;
}

This keeps dependencies consistent.

5. Confusing MVC layers with class count

Having more classes does not mean MVC is broken.

  • You can have many model-layer classes
  • You still have the same three high-level MVC responsibilities

6. Naming methods after implementation instead of intention

Less clear:

checkUsername()

Clearer:

usernameExists()
canRegisterUsername()

Good names reveal what the method answers.

Comparisons

Here is a practical comparison of common model-layer parts.

PartMain purposeUsually containsUsually should not contain
Entity / Domain ObjectRepresent application dataProperties, simple domain behaviorRaw SQL, HTTP request handling
Repository / DAOLoad and save dataSQL, PDO calls, persistence logicHTML output, request handling
ServiceBusiness rules and workflowsValidation, coordination, use-case logicDirect view rendering
ControllerHandle incoming requestCall services, choose response/viewSQL-heavy logic, domain rules scattered everywhere

Fat model vs separated model layer

ApproachProsCons
One large model classSimple at firstBecomes hard to maintain as app grows
Entity + Repository + ServiceClear responsibilities, easier testingMore classes to understand initially

Active Record vs Data Mapper style

StyleDescriptionExample idea
Active RecordObject often contains both data and persistence methodsUser->save()
Data Mapper / RepositoryData object is separate from persistence logicUserRepository->save($user)

Both styles exist in real projects. For maintainability, many developers prefer separating entities from raw database access when applications become larger.

Take Quiz

Cheat Sheet

Quick reference

  • In MVC, the Model is the application's data and business logic.
  • The model layer can contain multiple classes.
  • A model is not always a single class mapped directly to a table.

Good separation

  • Entity: represents data
  • Repository / DAO: performs database queries
  • Service: applies business rules
  • Controller: handles requests and delegates work

Good PHP example

class UserRepository
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function usernameExists(string $username): bool
    {
        $stmt = $this->pdo->prepare(
            "SELECT 1 FROM users WHERE username = :username LIMIT 1"
        );
        $stmt->execute(['username' => $username]);

        return (bool) $stmt->fetchColumn();
    }
}

Rules of thumb

  • Do not put SQL in controllers
  • Do not mix HTML output with model logic
  • Do not catch and rethrow the same exception unless adding context
  • Prefer dependency injection over passing the connection into every method
  • Use method names that express intent, like findById() or usernameExists()

Important edge case

  • Adding repository or service classes does not mean you no longer use MVC.
  • It simply means the model layer has internal structure.

FAQ

What should a model contain in MVC?

A model contains application data and logic related to that data. In practice, this may include entities, repositories, and services.

Is a model the same as a database table?

No. A database table is storage. A model represents application concepts and rules, which may be broader than a single table.

Should SQL go in the model or controller?

It should generally go in the model layer, usually in a repository or data access class, not in the controller.

Is it okay to separate database access into another class?

Yes. This is a common design choice and usually improves maintainability.

Do extra classes mean MVC becomes four layers?

Not necessarily. You still have Model, View, and Controller at the architectural level. You are just organizing the model layer internally.

Should an entity class talk directly to the database?

It can in some patterns, but many projects keep entities separate from persistence logic for cleaner design.

Why is catch and throw the same exception usually unnecessary?

Because it does not change the behavior. It only adds code unless you log, wrap, or add useful context.

What is a repository in PHP MVC?

A repository is a class responsible for fetching and saving data, often using PDO or an ORM.

Take Quiz

Mini Project

Description

Build a small username availability checker in PHP that demonstrates a clean MVC-style model structure. The project should show how a controller can ask a service whether a username is available, while the repository handles the database lookup. This mirrors a real feature used in registration forms.

Goal

Create a simple PHP program where username validation and database access are separated into clear model-layer classes.

Requirements

  • Create a UserRepository class that checks whether a username exists.
  • Create a UserService class that decides whether a username can be registered.
  • Create a UserController class that prints a user-friendly result.
  • Use PDO with a prepared statement.
  • Reject empty usernames before querying the database.
Take Quiz

Keep learning

Related questions

View more