A tiny MVC site structure with plain PHP

Example of a small site following MVC patterns in plain PHP

Posted on March 15, 2021 · 12 mins read

Motivation

You have just built a landing page for a customer. Design was pretty straightforward and they told you this was just a one pager without any extra functionality. In that moment you decided to do it with plain HTML + CSS and a bit of Javascript here and there.

Everything went just fine but one week later they ask for a small subscribe or contact form, an additional page or a tiny quiz to be added to your page. “Something very easy for you” they say.

Depending on the PHP frameworks / CMS you are skilled on its pretty simple to convert the page into it. My natural environment is Drupal and it does not make much sense to setup an entire Drupal site just for such a simple site.

In this post I will explore one of the infinite ways to do it in an MVC way in plain old PHP. It is an option we developers usually don’t consider but it’s actually pretty fast and funny to implement.

Requirements

Our site will not have any login. Only the following three pages:

  • Front: Front page that was our initial landing.
  • Details: A details page which should only be accessible if front has been viewed.
  • Feedback form: User will be allowed to contact us to get more info.

Autoload and front script

The only thing we will use from modern PHP web apps will be autoload of classes provided by composer to avoid all include_once which nightmared us not so many years ago. Also when needed we can easily add libraries in the future .

$ composer init

We can add our classmap directory to the generated composer.json:

{
    "require": {},
    "autoload": {
        "classmap": ["src"]
    }
}

composer install will set up the autoload for us and now we can set up our initial front script:

index.php:

require_once 'vendor/autoload.php';

// Load app settings
/** @var array $settings */
require_once 'settings.php';

$result = (new App($settings))->handle();

App controller

The App instance will contain most of our code. In case the web app grows some refactoring into multiple classes will be needed but for a tiny site it will be clear and maintainable enough.

It could be something like this:

src/App.php:

class App {

  /**
   * @var array
   */
  protected $settings;

  /**
   * Bootstrap any tools the app is using 
   * @param $settings
   */
  function __construct($settings) {

    session_start();

    $this->settings = $settings;
  }

  /**
   * Handles the request
   *
   * @return string[]
   */
  function handle() {

    return [
      'page_title' => 'Interesting front page',
      'html' => $this->renderContent('pages/front.php'),
    ];
  }
  
  
  /**
   * Builds HTML for a specific page
   * 
   * @param $page
   * @param array $vars
   *
   * @return false|string
   */
  protected function renderContent($page, $vars = []) {

    $base_url = $this->settings['base_url'];

    $app = $this;
    foreach ($vars as $key => $value) {
      $$key = $value;
    }
    ob_start();
    include $page;
    $contents = ob_get_contents();
    ob_end_clean();
    return $contents;
  }
}

As you can see, class constructor stores settings and performs whatever initialization tasks the application may require.

Most important function is handle which will be the app controller and will contain custom logic related to user input or session state. It will return an array where most important key is ‘html’ which is the markup which forms the page.

This markup will be formed in the renderContent function reading a PHP template containing mostly markup but with a bit of PHP on it. For example the front page markup might be:

pages/front.php:

<?php
/**
 * @var string $base_url
 */
?>
<main class="front">
  <p>Our products are really great !</p>
  <p><a class="btn btn-info btn-lg" role="button" href="<?php echo $base_url; ?>/details">Have a look</a></p>
</main>

Available responses: HTML and redirection

In this site there will be only two possible response types, redirection and HTML web content. In true sites there will be other options, probably json responses, spreadsheet exports, etc … Our site shouldn’t be much difficult to extend for such functionalities.

For the HTML response type, it would be advisable to render the whole content enclosed in buffers as in renderContent function but we will drop a special gift for those who may be responsible in the future for this code. If they don’t have much knowledge on PHP and need to make small changes in the layout it will be great to find the layout html in the main index.php file. I’m sure they will be able to do their modifications on it:

Final index.php:

<?php
require_once 'vendor/autoload.php';

// Load app settings
/** @var array $settings */
require_once 'settings.php';

$result = (new App($settings))->handle();

// Result array contains the 'url' key it is a redirect
if (isset($result['url'])) {
    header('Location: ' . $result['url']);
    exit();
}

// Otherwise it will contain 'html' key and is "render html"
// 'page_title' key is also expected
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="Marc Orcau">
    <title>Give us some feedback</title>

    <!-- Bootstrap core CSS -->
    <link href="assets/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="assets/styles.css" rel="stylesheet">
</head>
<body class="text-center">

    <div class="container">
        <a href="<?php echo $settings['base_url']; ?>"><img class="mb-4" src="assets/logo.png" alt="" width="80" height="80" /></a>
        <h1 class="h3 mb-3 fw-normal"><?php echo isset($result['page_title']) ? $result['page_title'] : $settings['page_title_default']; ?></h1>
      <?php echo $result['html']; ?>
    </div>

</body>
</html>

Application logic

Now we can add some application logic in the handle method and that’s it !!

  /**
   * Handles the request
   *
   * @return string[]
   */
  function handle() {

    // MySQL error
    if (!$this->mysqli()) {
      return ['html' => $this->renderContent('pages/error.php')];
    }

    switch ($_SERVER['REQUEST_URI']) {
      case '/details':
        if (!$this->getState('front-viewed')) {
          return ['url' => $this->settings['base_url'] . '/'];
        }
        return [
          'page_title' => 'Details page',
          'html' => $this->renderContent('pages/details.php'),
        ];
        break;
      case '/form-feedback':
        // A form was submitted
        if (!empty($_POST)) {
          $this->storeFeedback($_POST);
          $this->setState('front-viewed', NULL);
          return [
            'url' => $this->settings['base_url'] . '/',
          ];
        }
        return [
          'page_title' => 'Please give us some feedback',
          'html' => $this->renderContent('pages/form-feedback.php'),
        ];
        break;

      case '/':
        $this->setState('front-viewed', true);
        return [
          'page_title' => 'Interesting front page',
          'html' => $this->renderContent('pages/front.php'),
        ];
        break;
    }

    return [
      'url' => $this->settings['base_url'],
    ];
  }

Repository

You can get the code in following GitHub repository:

git clone budalokko/tiny-mvc-structure

Final thoughts

The reason frameworks and CMS begun to appear something around 15 years ago was the implementation of complex sites in a sustainable and scalable manner. This was really accomplished and PHP has proved to be a valid option to build serious apps but sometimes we don’t really need the overhead provided by all these years of making those frameworks/CMS better. In this situation it might be useful (in the sense of developer time) to go back to the roots.

Actually we could make the code a bit nicer and robust just dropping two or three composer packages on it and using them. Those which I would really add are:

  • symfony/http-foundation: Provides us Request, Response and Session objects making the code more OO and nice to work with. Probably my handle function will be much more readable and sustainable.
  • doctrine/orm: Provides some abstraction around dabase access

Comment on this post