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.
Our site will not have any login. Only the following three pages:
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();
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>
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>
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'],
];
}
You can get the code in following GitHub repository:
git clone budalokko/tiny-mvc-structure
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:
handle
function will be much more readable and sustainable.
Comment on this post