Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

MVC: Separate Concerns

Recommended resources

Hello! Welcome back to your course on PHP mastery.

In our last lesson, we built an object-oriented Router that directs incoming requests to specific controller methods. This was a major step in structuring our application. However, our controller methods are still generating HTML directly, which mixes application logic with presentation.

Today, we'll address that by implementing the full Model-View-Controller (MVC) pattern. Your learning outcome is to separate business logic (Model), presentation (View), and application flow (Controller). We will refactor our application so that each component has a single, clear responsibility.

This separation is the cornerstone of modern web application architecture, leading to code that is far more organized, maintainable, and scalable.

1. Understanding the Roles of M, V, and C

Before we write any code, let's solidify our understanding of what each part of the MVC triad does.

MVC Architecture Flow Diagram
A diagram showing the typical flow in an MVC pattern. The User's request goes to the Controller, which uses the Model to handle data and business logic. The Controller then passes data to the View, which generates the presentation sent back to the User.

To get a detailed definition of each component's responsibility, please read the following sections from the article "The MVC Pattern and PHP" from SitePoint.

The MVC Pattern and PHP, Part 1 - SitePoint

This article provides a clear breakdown of each component in the MVC pattern, including what they do and, just as importantly, what they don't do.

Please read the sections titled 'Model', 'View', and 'Controller'. Focus on understanding the specific role of each part and the boundaries between them.

Based on your reading, here is a summary of the core responsibilities:

  • Model: This is the "data brain" of your application. It's responsible for all business logic, including retrieving, storing, and manipulating data (e.g., from a database). It is "blind" to how the data will be displayed.
  • Controller: This is the "traffic cop." It receives the user's request from the router, communicates with the Model to get or update data, and then selects a View to render the final response. It does not contain any HTML or database queries itself.
  • View: This is the "presentation layer." Its only job is to display the data it is given by the controller. It's typically a file containing HTML and simple PHP echo statements for displaying variables. It should not perform any business logic.

Now, let's apply this separation to the application we started building in the last lesson.


2. Separating Presentation: Implementing the View

Our first step is to pull the HTML out of our HomeController.

Step 1: Create a views Directory

First, create a new directory named views in the root of your project. Inside this directory, create two files: home.php and about.php.

Move the HTML from the HomeController methods into these new files.

views/home.php

<h1>Welcome to the Homepage!</h1>
<p>This content is being served by the HomeController.</p>

views/about.php

<h1>About Us</h1>
<p>This is the about page, served by the HomeController.</p>

Step 2: Implement a View Class with Output Buffering

How does a controller render one of these files? We could use include or require, but that would output the HTML directly. We want the controller to get the rendered HTML as a string, which can then be returned to the router and finally echoed in our index.php.

To achieve this, we'll create a View class that uses a powerful PHP feature called output buffering.

The following video demonstrates exactly how to do this. It shows how to create a View class with a render method that cleverly uses ob_start() and ob_get_clean() to capture a file's output into a variable.

PHP MVC Pattern - View Parameters Exploit - Full PHP 8 Tutorial

Let's watch how to create a dedicated View class to handle rendering and separate our presentation files from our controllers.

Watch the segment from 05:20 to 10:08. Pay close attention to how the render method is built using output buffering (ob_start(), include, ob_get_clean()) to return the view's content as a string.

Let's create our own View.php file based on this concept.

View.php

<?php

class View
{
    protected string $viewFile;

    public function __construct(string $view)
    {
        // We'll define a constant for our views path for convenience
        if (!defined('VIEW_PATH')) {
            define('VIEW_PATH', __DIR__ . '/views/');
        }

        $this->viewFile = VIEW_PATH . $view . '.php';

        if (!file_exists($this->viewFile)) {
            // In a real app, you'd throw a custom exception
            throw new Exception("View file not found: " . $this->viewFile);
        }
    }

    public function render(): string
    {
        // Turn on output buffering
        ob_start();

        // Include the view file. Its output will be captured in the buffer.
        include $this->viewFile;

        // Get the contents of the buffer and clean it
        return ob_get_clean();
    }
}

Step 3: Update the Controller to Use the View

Now, we can update our HomeController to use this new View class. It will no longer contain any HTML.

You'll need to require_once 'View.php'; in your public/index.php for now.

controllers/HomeController.php

<?php

class HomeController
{
    public function index()
    {
        $view = new View('home');
        return $view->render();
    }

    public function about()
    {
        $view = new View('about');
        return $view->render();
    }
}

Finally, we need to make a small but crucial change to our router's dispatch method from the last lesson. The controller method now returns the HTML string, so the dispatcher must echo it.

In Router.php, inside the dispatch method:

// ... inside the if (method_exists(...)) block
if (method_exists($controllerInstance, $methodName)) {
    // Call the controller method and echo its return value
    echo $controllerInstance->$methodName(); // The change is here!
} else {
//...

With these changes, you've successfully separated the View from the Controller!


3. Adding Business Logic: Implementing the Model

Our controllers are clean of HTML, but they still aren't doing much. Let's introduce a Model to provide some data. For this lesson, our Model will simply return a hardcoded array of data.

Step 1: Create a Simple Model

Create a new directory called models. Inside it, create a PostModel.php file.

models/PostModel.php

<?php

class PostModel
{
    /**
     * In a real application, this method would query the database.
     * For now, it just returns a static array of posts.
     */
    public function getPosts(): array
    {
        return [
            ['id' => 1, 'title' => 'First Blog Post', 'content' => 'This is the content of the first post.'],
            ['id' => 2, 'title' => 'Second Blog Post', 'content' => 'This is some more interesting content.'],
            ['id' => 3, 'title' => 'Third Blog Post', 'content' => 'And here is the final post for our example.']
        ];
    }
}

Step 2: Pass Data from Controller to View

Now, how do we get this data from the PostModel to our home.php view? The Controller acts as the bridge.

  1. The Controller will instantiate the PostModel and get the data.
  2. The Controller will pass this data to the View class.
  3. The View class will make the data available as variables within the view file.

This process is explained very well in the next segment of the video. It also highlights a critical security concern that is directly relevant to your interest in CyberSec.

PHP MVC Pattern - View Parameters Exploit - Full PHP 8 Tutorial

Now let's see how to pass data from the controller to the view. This segment also contains an important demonstration of a potential security vulnerability when using the extract() function.

Watch the segment from 14:40 to 19:50. Focus on how the data array is passed to the View constructor and how extract() makes the data available. Pay very close attention to the security exploit demonstrated at 17:40. This is a classic example of why you must never trust user input.

Let's update our View class to accept and process this data.

View.php (updated)

<?php

class View
{
    protected string $viewFile;
    protected array $data;

    public function __construct(string $view, array $data = [])
    {
        // ... (constructor path logic is the same)
        $this->viewFile = VIEW_PATH . $view . '.php';
        $this->data = $data;

        if (!file_exists($this->viewFile)) {
            throw new Exception("View file not found: " . $this->viewFile);
        }
    }

    public function render(): string
    {
        // WARNING: Using extract() on user-provided data is insecure!
        // We use it here for demonstration, but ensure the $data array
        // passed from the controller is controlled by you, not user input.
        extract($this->data);

        ob_start();
        include $this->viewFile;
        return ob_get_clean();
    }
}

Now, update the HomeController to use the model and pass the data. Remember to require_once 'models/PostModel.php'; in your index.php.

controllers/HomeController.php (updated index method)

<?php

class HomeController
{
    public function index()
    {
        // 1. Get data from the Model
        $postModel = new PostModel();
        $posts = $postModel->getPosts();

        // 2. Pass data to the View
        $view = new View('home', ['posts' => $posts]);
        return $view->render();
    }

    // ... (about method is the same)
}

Finally, update your views/home.php file to display the posts. Because we used extract(['posts' => $posts]), a $posts variable is now available inside this file.

views/home.php (updated)

<h1>Welcome to the Homepage!</h1>
<p>Here are our latest posts:</p>

<ul>
    <?php foreach ($posts as $post) : ?>
        <li>
            <h2><?= htmlspecialchars($post['title']) ?></h2>
            <p><?= htmlspecialchars($post['content']) ?></p>
        </li>
    <?php endforeach; ?>
</ul>

(Note the use of htmlspecialchars(). This is crucial for preventing XSS attacks, a topic we'll cover in detail in the security module.)

You have now completed the full MVC separation!


Conclusion

Fantastic work. You've taken a script that mixed all its concerns and refactored it into a clean, professional MVC structure. This is a fundamental skill for any serious PHP developer.

Key Takeaways:

  • Model: Your PostModel encapsulates all business logic related to posts. The controller simply asks it for data.
  • View: Your views/home.php file is now a pure presentation template. It has no logic other than displaying the data it's given. The View class uses output buffering to render these templates into strings.
  • Controller: Your HomeController is the lean intermediary. It handles the request, gets data from the Model, and tells the View to render. It contains no HTML and no SQL.
  • Security: You've seen a practical example of how seemingly convenient functions like extract() can introduce vulnerabilities if used with untrusted data.

Preview of the Next Lesson:

Our views are separate, but what if you want every page to have the same header and footer? Copying and pasting is not a good solution. In the next lesson, "Create reusable view templates and partials," we will build a simple template inheritance system. This will allow us to define a master layout and have our individual views fill in the content, making our code even more efficient and easier to maintain.