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.
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
Userrepresents dataUserRepositoryhandles SQLUserServicehandles rulesUserControllerhandles 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
PDOdatabase 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
alicealready exists. - The repository runs this query:
SELECT 1 FROM users WHERE username = :username LIMIT 1
- If the query finds a row,
usernameExists()returnstrue. - The service negates that result and returns whether the username is available.
- The controller prints either
AvailableorAlready 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.
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.
| Part | Main purpose | Usually contains | Usually should not contain |
|---|---|---|---|
| Entity / Domain Object | Represent application data | Properties, simple domain behavior | Raw SQL, HTTP request handling |
| Repository / DAO | Load and save data | SQL, PDO calls, persistence logic | HTML output, request handling |
| Service | Business rules and workflows | Validation, coordination, use-case logic | Direct view rendering |
| Controller | Handle incoming request | Call services, choose response/view | SQL-heavy logic, domain rules scattered everywhere |
Fat model vs separated model layer
| Approach | Pros | Cons |
|---|---|---|
| One large model class | Simple at first | Becomes hard to maintain as app grows |
| Entity + Repository + Service | Clear responsibilities, easier testing | More classes to understand initially |
Active Record vs Data Mapper style
| Style | Description | Example idea |
|---|---|---|
| Active Record | Object often contains both data and persistence methods | User->save() |
| Data Mapper / Repository | Data object is separate from persistence logic | UserRepository->save($user) |
Both styles exist in real projects. For maintainability, many developers prefer separating entities from raw database access when applications become larger.
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()orusernameExists()
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.
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
UserRepositoryclass that checks whether a username exists. - Create a
UserServiceclass that decides whether a username can be registered. - Create a
UserControllerclass that prints a user-friendly result. - Use
PDOwith a prepared statement. - Reject empty usernames before querying the database.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.