📖 PHP MVC Views

Creating Views

Every web site needs a way to display the contents of its pages. In MVC sites, the view manages the page display including the proper layout, styles and content placement. Many views are possible, but it is the job of the controller to load the view appropriate for the user request and to insert any data retrieved from the model.

File Directory Structure

Sometimes these views may be referred to as templates. They contain the design layout of the page. In some systems the view is loaded through a simple include() method, but like other resource calls in our app, we want to use a class to manage the view load.

New Viewer Class

Since the viewer class will be part of our app Framework, It needs to be in the Framework folder. Next, create the Viewer.php file in the Framework folder and add a render method to the controller file to call the Viewer class.

Viewer.php
<?php

namespace Framework;

class Viewer
{
    
}

New Viewer Object

Create a new instance of the Viewer object in the controller and include the class with the use Framework\Viewer; statement. This will refer the autoloader to load the Viewer class from the Viewer.php file.

Products.php
<?php
...
use Framework\Viewer;
...
    public function index()
    {
    ...
        $viewer = new Viewer;

        require "views/product-list.php";
    }

...

Test the output in the browser. Your products/index page should load normally.

The render() Method

Currently our app is still loading the view using the require "views/product-list.php"; statement. We need to provide a way to laod the view from the Viewer class. Let's add a new function to the Viewer class to load a view based on a template name we pass to it.

Viewer.php
<?php

namespace Framework;

class Viewer
{
    public function render(string $template)
    {
        require "views/$template";
    }
}
Products.php

Now we call the render method on the Viewer object, passing the name of the view template we want to display.

<?php
...
use Framework\Viewer;
...
    public function index()
    {
        $model = new Product;

        $products = $model->getData();

        $viewer = new Viewer;

        $viewer->render("product-list.php");
    }

...

At this point, you should get an error when trying to load the page.

Warning: Undefined variable $products in C:\xampp\htdocs\profherd\php\adv-php\views\product-list.php on line 13

This error is referring to the $products variable called in teh product-list.php view. It was in scope when we called the view from the Products.php controller, but not when we call the view from the Viewer.php view. We will correct this by passing an array to the render function with the $products information.

Viewer.php
<?php

namespace Framework;

class Viewer
{
    public function render(string $template, array $products)
    {
        require "views/$template";
    }
}
Products.php

We will also need to pass the $products information in to the render() call in the Products controller.

<?php
...
use Framework\Viewer;
...
    public function index()
    {
        $model = new Product;

        $products = $model->getData();

        $viewer = new Viewer;

        $viewer->render("product-list.php", $products);
    }

...

Code Base Flexibility

This works well for our products model/view/controller. However, the may be times when we want to pass other data to the view file or maybe no data at all. We should update our code at this point to allow for these options. This is easy to do. We will now pass the data in an associative array with the name of the array element matching the name of the variable in the view. In this case, our view is looking for an array named $products. We will name our data array the same.

Convert Array Elements into Named Variables

We want to pass array information to the render method without changing the name of the variable in the viewer. To do this, we can use the PHP extract() function to convert the elements of an associative array into name=value pairs using the index name of the array element for the variable name.

$data = ['name'=>'Stephen', 'color'=>'Green'];

extract($data);

$name = 'Stephen';
$color = 'Green';
Viewer.php

First, change the name of the array passed to the class Viewer to something more generic and set its value to an empty array. We'll call our $data. Then extract the data from the  $data array. The EXTR_SKIP flag is set to skip elements of the same name to avoid duplicates.

<?php

namespace Framework;

class Viewer
{
    public function render(string $template, array $data = [])
    {
        extract($data, EXTR_SKIP);

        require "views/$template";
    }
}
Products.php

In the Products controller we will pass the data in as an associative array using the name of the view variable as the element name of the associative array.

<?php
...
use Framework\Viewer;
...
    public function index()
    {
        $model = new Product;

        $products = $model->getData();

        $viewer = new Viewer;

        $viewer->render("product-list.php", [
            "products" => $products
        ]);
    }

...

The View Template

We are currently loading the view template using a simply include "views/$template"; statement in the Viewer.php file. We want to load this content as a string instead of a file so we can better manage how we use the content.

If we were to use something like the PHP function file_get_contents() to read the contents of the view file and return the contents as a string and the file contains PHP code, it will not be parsed as such. It will be sent to the browser as if it were simple HTML. We don't want to send our PHP code to the browser so we will need a way of running the PHP code before we send the output to the browser.

Output Buffering

PHP provides a way of loading content into a buffer so we can build the page content into a string then output the content when we are ready. This is called output buffering. When we load a file using output buffering by calling ob_start(), the file will be parsed if it contains PHP code then saved into the buffer. When we are ready we simply call the contents of the buffer and clean the buffer using ob_get_contents(). This can be returned to the script and sent to the client browser.

Viewer.php

Add ob_start() before requiring the file and call ob_get_clean() to return the parsed file contents and clear the buffer.

<?php

namespace Framework;

class Viewer
{
    public function render(string $template, array $data = []): string
    {
        extract($data, EXTR_SKIP);

        ob_start();

        require "views/$template";

        return ob_get_clean();
    }
}
Products.php

Be sure to echo the contents of the output buffer to the response for viewing by the client.

<?php
...
use Framework\Viewer;
...
    public function index()
    {
        $model = new Product;

        $products = $model->getData();

        $viewer = new Viewer;

        echo $viewer->render("product-list.php", [
            "products" => $products
        ]);
    }

...

File Organization

We can now begin to move our view files into an organized file structure. In this activity we will create directories in our views directory where the name of the sub-directory corresponds to the name of the controller and the file name corresponds with the class. This is typical in many PHP frameworks.

One convention used to organize views is to create a directory structure that uses the name of the controller as the directory name and the name of the action as the view filename. This way it is easier to find the view associated with a particular controller.

Since we have two controllers, Home and Products, we will create matching view directories. Then we move each of the matching views to their corresponding view directories and rename the files to match the controller's action.

File System Map
ControllerActionOld View FilenameNew View DirectoryNew View Filename
Home.php index() home-index.php Home index.php
Products.php index() product-list.php Products index.php
Products.php show() product-show.php Products show.php
Sample File Directory Structure

File Directory Structure

↧ VIEWS (SITE ROOT)
↳ src
| ↳ App
| | ↳ Controllers
| | | ↳ Home.php
| | | ↳ Products.php
| ↳ Framework
↳ views
| ↳ Home
| | ↳ index.php
| ↳ Products
| | ↳ index.php
| | ↳ show.php
↳ .htaccess
↳ index.php

Update the Controllers

Each controller needs to be updated to point to the appropriate view file in the new views directory structure. Make sure you add the appropriate use Framework\Viewer; statement to import the Viewer class into each controller.

Products.php Controller
<?php

namespace App\Controllers;

use App\Models\Product;
use Framework\Viewer;

class Products
{
    public function index()
    {
        $model = new Product;

        $products = $model->getData();

        $viewer = new Viewer;

        echo $viewer->render("Products/index.php", [
            "products" => $products
        ]);
    }

    public function show()
    {
        $viewer = new Viewer;

        echo $viewer->render("Products/show.php");
    }
}
Home.php Controller
<?php

namespace App\Controllers;

use Framework\Viewer;

class Home
{
    public function index()
    {
        $viewer = new Viewer;

        echo $viewer->render("Home/index.php");
    }
}

Template Views

In every web site there are common page elements. These usually include the head section, main page header, navigation menu and footer. It is beneficial to extract these elements into a separate template file to improve site maintenance. Having these elements in one place makes updating our site much easier.

In order to access this content from many page views, we are going to create a new sub-directory called shared to store our shared template files. Place this directory in the existing views directory.

Create a new file in the shared directory called header.php. Take a look at the Products/index.php file. Extract all the HTML contents that would be common to the other pages. This will usually include all content above the main page heading. Cut this content and paste it into a new file called header.php.

Contents of header.php
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Products</title>
</head>

<body>

Now we can add a new render() call to the header.php view from the Products controller and load it before we load the Products/index.php view. Do the same for the other views.

echo $viewer->render("shared/header.php");

We will leave the closing </body> and </html> tags in the view for now.

Dynamic Page Titles

Currently, our new page header template has the <title> tag hard coded into the header.php file. We want this to be dynamic so we can provide a more appropriate title for each page.

We begin by modifying the header.php file to add a variable to the <title> tag.

<title><?= $title ?><title>

Now we can pass a title value in the controller when we call the render() method.

Home.php Controller
<?php

namespace App\Controllers;

use Framework\Viewer;

class Home
{
    public function index()
    {
        $viewer = new Viewer;

        echo $viewer->render("shared/header.php", [
            "title" => "Home"
        ]);

        echo $viewer->render("Home/index.php");
    }
}

Update the Products controller similarly. Now test your site pages to make sure they render a complete HTML page with the matching page title. If all works well, you have created a site view template approach for rendering your site pages.

Views with Data

Many of your site pages will need data from the database. In order to use data from the database, you will need to call a model method to query the database and return the data to the view. In this case, we will call the model class before rendering the page and pass the data to the view template in an array that matches the database query result set. Below you will see an example of the Products controller calling the Product model for data from the database and passing the result set to the view.

Product Model Example

Note that the database connection is contained in a callable class to be available for multiple queries.

<?php

namespace App\Models;

use PDO;

class Product
{
    // connect to the database
    public function getConnection()
    {
        $dsn = "mysql:
                host=localhost;
                dbname=adv_php;
                charset=utf8;
                port=3306";

        return new PDO($dsn, "adv_php_user", "secret", [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]);
    }

    // method to gat all rows of data from the products table
    public function getData(): array
    {
        // establish db connection
        $conn = $this->getConnection();

        // create db query
        $sql = "SELECT * FROM `products`";

        // send query to db
        $stmt = $conn->prepare($sql);

        // execute query
        $stmt->execute();

        // return db result set to Products controller as associative array
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    // method to get a row(s) from db based on a specific ID 
    // passed from the index.php page in the site root directory
    public function find(string $id): array|bool
    {
        $conn = $this->getConnection();

        $sql = "SELECT * FROM `products` WHERE id = :id";

        $stmt = $conn->prepare($sql);

        $stmt->bindValue(":id", $id, PDO::PARAM_INT);

        $stmt->execute();

        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
}
Update index.php Example

Add code to pull out the id parameter from the URL path segment if it exists. Add this code after the parameters are pulled from the URL segments and before the action/controller are set.

<?php
...
$params = $router->matchRoute($path);

if ($params === false) {

    throw new PageNotFoundException("No matching route for '$path'.");
    
}

if ( !empty($params["id"]) ) {

    $id = $params["id"];

} else {

    $id = NULL;

}

$action = $params["action"];
$controller = "App\Controllers\\" . ucwords($params["controller"]);
...
Products Controller Example
<?php

namespace App\Controllers;

use App\Models\Product;
use Framework\Viewer;

class Products
{
    public function index()
    {
        // create new model object
        $model = new Product;

        // call getData() method from product model
        // assign result set to $products variable
        $products = $model->getData();

        if ($products === false) {

            throw new PageNotFoundException("Product not found");
            
        }
                $viewer = new Viewer;

        echo $viewer->render("shared/header.php", [
            "title" => "Products"
        ]);

        // pass db result set to view in array named "products"
        echo $viewer->render("Products/index.php", [
            "products" => $products
        ]);
    }

    public function show(string $id = NULL)
    {
        $model = new Product;

        $product = $model->find($id);

        if ($product === false) {

            throw new PageNotFoundException("Product not found");
            
        }        $viewer = new Viewer;

        echo $viewer->render("shared/header.php", [
            "title" => "Product"
        ]);

        // pass db result set to view in array named "product"
        echo $viewer->render("Products/show.php", [
            "product" => $product
        ]);
    }
Update View Example

Note that for views where the data passed is in a table format (mulitple records) a foreach() loop is required to step through and render each record. For data that is passed as a single record, use the name passed by reference from the render() view call in the controller.


<!-- index.php view -->
    <nav>
        <ul>
            <li><a href="/">Home</a></li>
        </ul>
    </nav>
    <h1>Products</h1>
    <?php foreach ($products as $product) : ?>
        <p>
            <a href="/products/<?= htmlspecialchars($product['id']) ?>/show">
                <?= htmlspecialchars($product["name"]) ?>
            </a>
        </p>
    <?php endforeach; ?>

</body>
</html>

<!-- show.php view -->
    <nav>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/products">Products</a></li>
        </ul>
    </nav>
    <h1>Show Product Page</h1>
    <h2><?= $product["name"] ?></h2>
    <p><?= $product["description"] ?></p>

</body>
</html>