Eén van de eerste uitdagingen waardoor ik geïntrigeerd ben als startende programmeur is: hoe kan ik zelf een CMS (content management systeem) bouwen. Bij Insyde, waar ik momenteel 2 dagen in de week werk, hebben we namelijk ook een eigen CMS en dat heeft iets erg interessants.

Je kunt zelf bepalen hoe je gebruiksvriendelijkheid, framework en modules eruit komen te zien. En je dient alles vanaf scratch op te bouwen. Die uitdaging wilde ik graag ook zelf aangrijpen en dus startte ik mijn missie om een Daan CMS te bouwen. 

Daan CMS is een CMS zonder framework, puur gefundeerd vanuit klassieke PHP en een database. Daan CMS heeft nog geen echte duidelijke vorm, maar door de afgelopen tijd veel te lezen over de basisfuncties van een beheersysteem voor websites, heb ik een goed beeld gekregen welke onderdelen een eigen CMS moet bevatten om goed te functioneren. 

En dat wil ik vandaag graag delen. Mijn eigen gedistilleerde samenvatting van hoe je stapsgewijs een eigen CMS voor een website kunt maken met behulp van object geörienteerde PHP en MySQL. Ik zal werken binnen een MMVC-model (Model-Mapper-View-Controller) voor de onderlinge verhoudingen.

De bestanden

Download de bestanden die bij dit artikel horen vanaf mijn Github. Ik kopieer niet alle code in dit artikel namelijk en dan zie je direct de context waarbinnen ik code snippets uitlicht. 

Stap 1: Autoloader opzetten

Wanneer je inmiddels een tijdje bezig bent met programmeren, ben je vast het concept van 'autoloading' tegengekomen. Er komt namelijk een moment dat je denkt, moet ik nu alweer requires schrijven voor alle bestandden die ik nodig heb?

require 'models/ContentModel.php';
require 'config/Settings.php';
require 'views/ContentView.php';

Nee dus! :) Er is namelijk autoloading geïntroduceerd binnen de nieuwere versies van PHP die object geörienteerd programmeren een stuk fijner maakt. Via autoloading worden al je classes automatisch ingeladen, tenminste, als je ook goed gebruikmaakt van namespaces en use binnen je bestanden.

Een voorbeeld snippet ziet er dan als volgt uit:

<?php
namespace Controllers;

use \Models\ContentModel;

class ContentController
{
  public function __construct (ContentModel $model)
  {
    // ..
  }

// ..
}

?>

Het zorgt ervoor dat je, zolang je namespaces gebruikt, waar je maar wilt nieuwe classes kunt aanroepen zonder requires! Daarnaast kun je de annotatie binnen de classes zelf korter houden door use te gebruiken. Dit voorkomt dat er in constructor '\Models\ContentModel $model' moet worden gebruikt. En het voorkomt dat als we later nog een keer ons ContentModel aan moeten roepen, we weer de lange notatie moeten gebruiken.

Maar.. voordat we op deze manier kunnen gaan programmeren hebben we eerst nog een echte autoloader nodig! Gelukkig is er een universele autoloader die dit voor ieder PHP project regelt en die je niet per se zelf hoeft te maken (maar dit mag natuurlijk altijd!). Deze vind je hier: https://gist.github.com/jwage/221634.

Sla deze op in 'Autoloader/Loader.php'.

Stap 2: Starten met onze index.php

Ieder project heeft een goede fundering nodig en die van ons bestaat uit (natuurlijk) een traditioneel index.php bestand. Vanuit hier laden we straks een aantal belangrijke basisonderdelen in die we nodig hebben voor het ontwikkelen van functionaliteit binnen de website. 

Op dit moment hebben we één basisonderdeel, dus laten we daarmee starten!

index.php

<?php
require 'Autoloader/Loader.php';

$loader = new SplClassLoader();
$loader->register();

Vanaf nu is onze autoloader actief geregistreerd via de PHP functie 'spl_autoload_register()' (aangeroepen door $loader->register()) en kan de magie beginnen.

Stap 3: Verbinding met de database maken

We hebben als CMS later de functionaliteit nodig die data kan verwerken met de traditionele CRUD (Create / Read / Update / Delete) operaties. De meest gebruikte manier om dit te doen is via een database en binnen PHP is het fijn om met PDO te werken hiervoor. 

PDO maakt de verbinding met de database en zorgt ervoor dat je binnen één regel code ervoor kunt zorgen dat je website aan een volledig nieuwe database gekoppeld kan worden voor bijv. testen of omdat je wilt experimenteren met iets anders dan MySQL, zoals SQLite.

Laten we eens zien hoe dit in zijn werk gaat.

Config/Settings.php

<?php

$servername = "localhost";
$username = "root";
$password = "";
$dbname = "jouw_database";

try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    echo "Connection failed: " . $e->getMessage();
}

We proberen hierboven een verbinding op te zetten via PDO en wanneer dit faalt, dan krijgen we een nette foutmelding. Vergeet natuurlijk niet je eigen MySQL gegevens in te vullen voor het maken van de verbinding :) en alvast een lege database aan te maken met de gekozen $dbname.

Om dit in onze applicatie te krijgen dienen we de laatste require te plaatsen binnen onze index.php. Voeg daar dus de volgende regel toe, na de andere require voor de autoloader.

require 'Config/Settings.php';

Stap 4: Even nog wat basis-instellingen

Nu we toch al een handig settings-bestand hebben, is het tijd om deze direct te benutten voor een aantal andere basis-instellingen die we later nodig gaan hebben. Breid het bestand als volgt uit.

Config/Settings.php

// error-reporting aanzetten - erg handig voor debuggen van code
// zet met comments uit wanneer je deze niet wilt laten zien in je live-omgeving
error_reporting(E_ALL);
ini_set('display_errors', 1);

// dit zorgt ervoor dat we later in ons CMS niet zo snel worden uitgelogd
// een standaard sessie loopt namelijk maar 20 minuten ongeveer!
// nu zetten we deze naar 1 uur (3600 seconden)
ini_set('session.gc_maxlifetime', 3600);

// gedurende het ontwikkelen is het handig om een constante te definiëren die
// dient als basis URL. Op deze manier voorkomen we zaken zoals relatieve URLs
// en hebben we een simpele plek waarin we de basis URL kunnen wijzigen wanneer het // project bijvoorbeeld op een andere plek komt te staan
// deze basis URL definieer je zonder localhost erin
define('BASE_URL', '/jouw-project-folder/');

Stap 5: Een eigen router maken

Eén van de laatste belangrijke bouwstenen die we nodig hebben voor het faciliteren van een soepele website, is het opzetten van een zogenaamde router. Een router zorgt ervoor dat een bepaalde URL wordt vertaald naar een controller en een actie

In mijn eigen CMS gebruik ik op dit moment een simpele versie, die nog veel beter kan, maar die voor een beginnende programmeur prima werkt. Hieronder een illustratie van hoe dit in zijn werk gaat.

Voorbeeld URL: /content/create

Het eerste deel van de URL is '/content/', dus de router weet dan dat deze de 'ContentController' moet aanroepen. Het tweede deel van de URL is de actie, in dit geval 'create', waardoor de ContentController weet dat deze binnen zijn methods (klasse-functies) moet gaan zoeken naar een create-functie. 

Wanneer je complexere functies schrijft, dan kun je via GET-parameters nog extra informatie meegeven aan het CMS, zodat deze gebruikt kunnen worden voor het verrijken van de functie.

Aangezien dit zo'n belangrijk en complex onderdeel is, kijk ik graag met je naar mijn eigen gebouwde 'FrontController' ofwel 'Router'. 

Controllers/FrontController.php

<?php
namespace Controllers;

class FrontController 
{
	public $conn;

	// database meegeven aan constructor, zodat deze later automatisch kan worden meegegeven aan de mapper
	// die beheert namelijk de acties die op de database worden uitgevoerd
	public function __construct(\PDO $conn)
	{
		$this->conn = $conn;
	}

	public function parse() {
		// eerst de URL clean maken, zodat de router niet meer afhankelijk is van domeinnamen / localhost / projectfolders / et cetera
		$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
		$url = str_replace(BASE_URL, "", $url);
		
		// laatste slash weghalen, mochten mensen die intypen, anders wordt dit later een probleem bij het opsplitsen
		// van de URL in controller/actie 
		$url = rtrim($url, "/");
		
		// alle GET-parameters weghalen uit deze lokale URL, deze roepen we later wel aan vanuit de controllers zelf
		$url = preg_replace("/\?(.*)/", "", $url);

		// tijd om de URL op te delen in stukjes en zo een verdeling te krijgen van in ieder geval controller/actie
		$request = explode("/", $url);

		// op basis van het controller-deel van de URL (request[0]) kijken we of deze bestaat, anders moet onze applicatie niets doen
		$controllerFilename = "Controllers/" . ucfirst($request[0]) . "Controller.php";
		if(file_exists($controllerFilename)) {

			// URL nog meer opschonen door de controller eruit te halen, zodat alleen de paginanaam nog overblijft
			// dit hebben we later nodig bij het tonen van bijvoorbeeld /content/over-ons, 
			// waarbij 'over-ons' over moet blijven, aangezien we kunnen opzoeken in de database
			$url = str_replace($request[0] . "/", '', $url);

			// genereer dynamisch het model, op basis van de controller
			// het model dient als dataweergave van een tabel in de database
			$model = "Models\\" . ucfirst($request[0]) . 'Model';
			$model = new $model();

			// genereer dynamisch de mapper, op basis van de controller
			// deze geven we database-verbinding mee en een model
			$mapper = "Mappers\\" . ucfirst($request[0]) . 'Mapper';
			$mapper = new $mapper($this->conn, $model);

			// genereer dynamisch de view, op basis van de controller
			// deze geven we ook het model mee, zodat de view de data uit het model kan gebruiken
			// voor het weergeven van bijvoorbeeld de contentpagina's
			$view = "Views\\" . ucfirst($request[0]) . 'View';
			$view = new $view($model);

			// genereer dynamisch de controller, met de overige 3 als afhankelijkheden (dependencies)
			// door deze zo mee te geven, gebruiken we een vorm van dependency injection, wat handig is bij
			// het eventueel later testen van de controller
			$controller = "Controllers\\" . ucfirst($request[0]) . 'Controller';
			$controller = new $controller($model, $mapper, $view);

			// hier checken we of de gegeven actie (request[1]), bestaat en zo ja, of deze actie ook bestaat
			// binnen de gekozen controller. Zo niet dan wijken we uit, naar een algemene functie 'actionShow', die later aan bod komt
			// deze zorgt namelijk voor de contentpagina's zoals 'over-ons'
			// is er helemaal geen actie aanwezig, dan wijken we uit naar de index van een pagina
			if(isset($request[1])) {
				$action = "action" . ucfirst($request[1]);
				if(method_exists($controller, $action)) {
					$controller->{$action}();
				} else {
					$controller->actionShow($url);
				}
			} else {
				$controller->actionIndex();
			}
		}
	}
}

Zo.. dat is flink wat code en nog lang niet optimaal, maar dat is ook juist het leuke. Zo kun je er zelf aan gaan sleutelen! :) Voor nu doet het in ieder geval het belangrijkste: het kiest op basis van de URL automatisch een controller & een klasse-functie uit.

Het enige wat we nu nog moeten doen is ervoor zorgen dat onze index.php deze FrontController kan aanroepen wat al automagisch gebeurd via onze autoloader!):

index.php

$router = new Controllers\FrontController($conn);
$router->parse();

Stap 6: Single Entry Point opzetten

Om ervoor te zorgen dat daadwerkelijk alle verzoeken netjes bij onze FrontController terecht komen, is er nog één extra stap vereist: het opzetten van een Single Entry Point

Alle verzoeken die gedaan worden naar onze server, moeten worden doorverwezen naar de index.php, zodat deze de verzoeken kan afhandelen via het systeem zoals hierboven beschreven staat. 

Ik werk zelf met een Apache-webserver (zoals een standaard LAMP / WAMP installatie) en gebruik hiervoor het misschien al bekende .htaccess-bestand. Deze wordt ingeladen voordat er daadwerkelijk bestanden worden opgevraagd van de server en geeft dus nog de optie om alle verzoeken te sturen naar onze index.php.

Dit is erg eenvoudig te doen met de volgende code. Sla de .htaccess op in de root van je project, waar ook je index.php staat.

.htaccess

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [QSA,L]

De code zegt eigenlijk, zolang de opgevraagde URL geen bestaand bestand of directory is, kun je alles doorverwijzen naar de index.php.

Stap 7: ContentController opzetten

Aangezien we bezig zijn met een content management systeem, is de belangrijkste controller die we op kunnen zetten de ContentController. Deze dient alle basisacties (CRUD) van het CMS goed op te vangen en uit te zetten. Laten we stap voor stap er doorheen lopen.

De basis

Controllers/ContentController.php

<?php

namespace Controllers;

use \Models\ContentModel;
use \Mappers\ContentMapper;
use \Views\ContentView;

class ContentController
{
	private $mapper;
	private $model;
	private $view;

	public function __construct(ContentModel $model, ContentMapper $mapper, ContentView $view)
	{
		$this->mapper = $mapper;
		$this->model = $model;
		$this->view = $view;
	}
 // ..
}

We starten met de gebruikelijke autoloading-magie via namespaces & use. Daarna maken we binnen onze ContentController class een constructor met daarin de drie andere onderdelen van het MMVC-model, het Model, de Mapper & de View. Deze heten allemaal 'Content' + ..' voor het behouden van een logische structuur.

Deze worden ingeladen via de constructor en toegekend aan lokale private properties, wat ook wel dependency injection wordt genoemd. Hierdoor zijn er geen afhankelijkheden binnen de code en kun je ook een 'nep'-class inladen van buitenaf, wanneer je aan het testen bent. 

Vanaf nu kunnen we deze dus overal in onze ContentController class gebruiken.

Onze eerste methode

 

Controllers/ContentController.php

public function actionIndex()
{
	$this->view->setTemplate('index');
	$this->view->render();
}

De actionIndex-methode komt na de constructor. Het doet eigenlijk iets heel simpels. Het zegt tegen de view dat deze de index-template moet gaan ophalen en vervolgens dat deze getoond mag gaan worden ('render'). 

Nu kennen we natuurlijk de ContentView-class nog niet en weten we nog niet precies wat deze actie doet. Tijd dus voor de volgende stap!

Stap 8: De ContentView opzetten

Binnen de ContentView gaan we gelijk nog een ander belangrijk principe van object geörienteerd programmeren toepassen, namelijk inheritance, ofwel overerving. Wanneer je namelijk met een CMS aan de slag gaat dan is de kans klein dat je alleen maar contentpagina's wilt tonen. 

Nu heb ik zelf gemerkt dat daardoor de diverse views aardig wat overlap hebben qua functionaliteit en het dus eigenlijk zonde is om dit overal te herhalen. Een poging tot het DRY-principe (Don't Repeat Yourself). 

Om dit te gebruiken creëren we een basis-view, als volgt.

Views/View.php

<?php

namespace Views;

use \Views\View;

class View
{
	protected $model;
	protected $template;
	protected $data;

	public function __construct(Model $model)
	{
		$this->model = $model;
	}

	public function setTemplate($template)
	{
	}

	public function render()
	{
		require_once 'Templates/Layout/Head.php';
		require_once $this->template;
		require_once 'Templates/Layout/Footer.php';
	}

	public function setData($data)
	{
		$this->data = $data;
	}
}

De functies spreken redelijk voor zich denk ik. We hebben de optie om een template in combinatie met onze header/footer in te laden via render() en we kunnen data meegeven aan onze view/templates, met behulp van setData. Functionaliteit die heel universeel te gebruiken is en waardoor onze uiteindelijke ContentView een stuk kleiner wordt. Kijk maar!

Views/ContentView

<?php

namespace Views;

use \Models\ContentModel;

class ContentView extends View
{
	public function __construct(ContentModel $model)
	{
		$this->model = $model;
	}

	public function setTemplate($template)
	{
		$this->template = 'Templates/Content/' . ucfirst($template) . '.php';
	}
}

We breiden de ContentView-class uit met de View-class via 'extends' en overrulen daarna gelijk twee methodes door deze opnieuw te definiëren binnen deze child-class (View is de parent). Dit geeft onze de mogelijkheid een specifiek ContentModel & Content-template in te laden. 

Nu snappen we dus wat de ContentController uit stap 7 probeerde te realiseren. Er dient een .php-template ingeladen te worden voor de specifieke actie, in dit geval 'index', aangezien we met actionIndex de homepage willen weergeven. Deze template wordt weer automatisch gerenderd in combinatie met een header & een footer met behulp van render().

Maar.. daar hebben we nog niet naar gekeken, dus tijd voor een korte blik in onze templates!

Stap 9: De templates maken

Dit is eigenlijk de meest eenvoudige stap, aangezien het meeste werk al op de achtergrond wordt gedaan. 

We starten met de index-template, die we net hebben aangeroepen.

Templates/Content/Index.php

<h1>Onze homepage!</h1>
<p>Hier kunnen we al onze designkunsten straks op loslaten</p>

Zoals je ziet wordt ook hier de structuur gehanteerd. Alle Content-templates in een mapje met /Content/, zodat deze via setTemplate goed worden ingeladen.

Maar er is meer, we hebben namelijk ook nog onze header & footer nodig die we overal kunnen inladen! Laten we snel even zorgen voor een basis.

Templates/Layout/Head.php

<!DOCTYPE html>
<html lang="nl">
<head>
	<meta charset="UTF-8">
	<title>Jouw project</title>
</head>
<body>
	<div id="container">
		<header class="header-home">
			<div class="logo">
				<a href="#">Logo</a>
			</div>
			<nav>
				<ul>
					<li><a href="#">Menu-item 1</a></li>
					<li><a href="#">Menu-item 2</a></li>
				</ul>
			</nav>
		</header>

 Templates/Layout/Footer.php

	</div>
</body>
</html>

En voilà, een simpele manier om de weergave van je pagina op te bouwen zonder overal requires en andere lelijke terugkerende code te moeten blijven schrijven. 

Stap 10: ContentModel opzetten

In principe hebben we alles al op zijn plek om de homepage te tonen (aangezien we nog geen database actief gebruiken), maar gezien onze automatische systeem, zijn we er nog niet helemaal! Er worden namelijk bij iedere controller ook een Model en een Mapper aangeroepen. En die bestaan nog niet, waardoor we tegen errors aanlopen. 

Tijd om daar verandering in te brengen. Wederom gaan we overerving gebruiken om onze Model-structuur op te zetten. 

Models/Model.php

<?php

namespace Models;

class Model
{
  private $_data;

  public function __construct(array $data = array())
  {
    if(empty($data))
      return;

    $this->_data = $data;

    foreach($this->_data as $column => $value) {
      $this->{$column} = $this->_data[$column];
    }

  }

  public function setValue($key, $value)
  {
    $this->{$key} = $value;
  }
}

Ons standaard Model is altijd een weergave van een database-tabel. Deze tabel bestaat uit verschillende kolommen en, zoals we later zullen zien, laten wij deze kolommen overeenkomen met de eigenschappen (properties) van het Model. 

De constructor van het Model bestaat dan ook altijd uit het omzetten van de data uit de database, naar de eigenschappen in het model. Dit is wat de foreach-functie doet. Hierdoor kunnen we later een stukje data, bijvoorbeeld de <h1> van een pagina, aanroepen via de syntax $model->h1, of $this->model->h1. 

Ten slotte bouwen in de standaard Model een functie in om andere eigenschappen toe te wijzen van buitenaf via de setValue-methode. Dit kan handig zijn wanneer we een bestaand model willen uitbreiden of veranderen op basis van een actie van de gebruiker, zoals bijvoorbeeld het uploaden van een plaatje. 

Door deze universele opzet van het standaard Model, hebben we voorlopig geen extra functionaliteit nodig binnen ons ContentModel. We kunnen alles overerven!

Models/ContentModel.php

<?php

namespace Models;

class ContentModel extends Model
{

}

En we hebben later uiteraard nog de optie om specifieke functionaliteit toe te voegen!

Stap 11: ContentMapper opzetten

Tot slot van onze basis hebben we nog een manier nodig om met de database te kunnen praten. De Mapper, of in dit geval ContentMapper, dient als laag tussen de controller en de database om dit te doen. Op die manier zijn de verantwoordelijkheden netjes gescheiden en probeert het CMS rekening te houden met weer een ander belangrijk principe van OOP, namelijk het Single Responsibility principe (SRP)

Dit principe zorgt voor duidelijkheid, aangezien je weet welk onderdeel van je code/klasse-systeem verantwoordelijk is voor wat. Wederom is dit een best practice in relatie tot testen, aangezien je dan duidelijk een klasse kunt testen op specifieke functionaliteit.

Voor onze index.php-template hebben we nog geen data uit de database nodig voor nu, dus laten we starten met een simpele opzet van de ContentMapper!

Mappers/ContentMapper.php

<?php

namespace Mappers;

use \Models\ContentModel;

class ContentMapper
{
	private $conn;
	private $model;

	public function __construct(\PDO $conn, ContentModel $model)
	{
		$this->conn = $conn;
		$this->model = $model;
	}
}

De $conn is onze database-verbinding, die doorgegeven is vanuit de router (weet je nog? :)). Het model is een object dat de data uit de database representeert. Het zou mooi zijn als we daarom via de structuur van het model de database in en uit de database kunnen zetten/halen. 

Vanuit hier gaan we in een later deel alle SQL-queries uitvoeren!

Stap 12: Tijd om te genieten!

Onze applicatie is in de basis klaar! Als je alles stapsgewijs gevolgd hebt, of al direct de bestanden hebt gedownload, dan is het nu tijd om onze index op te vragen. 

Ga naar localhost/jouw-project/content/ en voilà, hier zou nu een werkende template moeten worden ingeladen! :)