Vorlage und Anleitung: PHP mit MVC ohne Framework

Vorlage und Anleitung: PHP mit MVC ohne Framework

MVC ist ein bewährtes Muster zur Entwicklung von Anwendungen. Es schafft Übersicht und bringt eine gewisse Struktur in die Entwicklung. Soll eine PHP-Webanwendung nach dem MVC-Prinzip entwickelt werden, ist der Gedanke über ein MVC-Framework oft nicht mehr weit entfernt. Doch da fangen die Probleme bereits an: Welches Framework eignet sich am besten? Ein Bolide wie das Zend Framework? Oder doch lieber eine leichtgewichtigere Alternative wie CodeIgniter? Ist die Dokumentation brauchbar?  Und wie sieht es eigentlich mit Support/Kompabilität, geschweige denn einer Template-Engine aus?

Keine einfache Entscheidung. Frameworks haben durchaus ihre Daseinsberechtigung. Aber insbesondere bei kleineren Anwendungen ist ein umfangreiches Framework einfach zuviel des Guten und bremst die Anwendung und Entwicklung eher aus als sie zu fördern. Grundsätzlich braucht man zwar überhaupt kein Framework um nach dem MVC-Prinzip in PHP zu entwickeln. Aber eine Art Micro-Framework erleichtert die Arbeit in der Tat ungemein. Folgender Artikel stellt eines zusammen.

Struktur und Namenskonventionen

Zu Beginn macht es Sinn, sich Gedanken über die Verzeichnisstruktur zu machen. Die Folgende hat sich bewährt:

-Model
-View
–Global
-Controller
-Include
-Clientscript

JavaScript und CSS kann man natürlich in getrennte Ordner legen, mache ich jedoch bewusst nicht: Sie gehören oft zusammen und so viele gibt es davon auch wieder nicht, dass die Übersicht durch eine Aufteilung deutlich verbessert werden würde. Clientseitige Frameworks wie Bootstrap kommen meist ohnehin in einem eigenen Ordner daher. Diese aufzuteilen ist erfahrungsgemäß keine gute Idee, da beispielsweise in CSS-Dateien nicht selten auf weitere Ressourcen wie Schriften oder Grafiken verwiesen wird, welche die originale Verzeichnisstruktur voraussetzen. Der Aufwand überwiegt daher dem Nutzen.

Für die Bezeichnungen von Ordnern und Dateien sollte zu Beginn eine Namenskonvention aufgestellt und natürlich auch eingehalten werden. Das ermöglicht später nicht nur Automatisierung, sondern schafft Übersicht und vermeidet Fehler. Für diesen Artikel sowie das dazugehörige Template gilt folgende Namenskonvention:

  • UpperCamelCase für Dateien, Ordner und Klassen (z.B. Config.php, class Config)
  • lowerCamelCase für Attribute, Funktionen und Variablen (z.B. private $contentpublic function getContent())
  • Controller erhalten ein Controller-Prefix in Dateiname und Klasse (z.B. class ControllerHome, ControllerHome.php). Funktionsnamen entsprechen denen der Actions.
  • Views erhalten ein View-Prefix im Dateinamen(z.B. ViewHome) und werden in Unterordnern strukturiert, die nach dem dazugehörigen Controller benannt werden – Seitenbestandteile die von allen Controllern geteilt werden gehören in den Unterordner Global
  • Klassen werden gleich benannt wie die dazugehörige Datei, ohne Präfixe (z.B. class User => User.php)

Kontrolliere die Controller

Wer an seine Anfangszeit als PHP-Entwickler zurückdenkt, wird sich noch gut an Konstrukte nach dem folgenden Schema erinnern können:

if( $_GET['page'] == 'home' ) {
    // Inhalt der Home-Seite laden
}else if( $_GET['page'] == 'contact' ) {
    // Inhalt der Kontakt-Seite laden
}
// [...]

Spätestens wenn die Anwendung im Laufe der Zeit umfangreicher wurde, hatte man am Ende eine riesige Datei, die nur aus solchen If-Elseif bzw. Switch-Statements besteht – Vorzugsweise die Indexseite. Das ist unübersichtlich und schafft Arbeit. Warum also nicht diese lästige Routine automatisieren? PHP-Frameworks arbeiten hier häufig mit Routen. Etwas vom Ansatz her vergleichbares lässt sich jedoch auch recht einfach selbst umsetzen. Dazu ist es wie in der Namenskonvention vorgegeben nötig, dass jede Action über eine gleichnamige öffentliche Methode im dazugehörigen Controller verfügt.

Ein Beispiel:

class ControllerHome {
    public function index() {
        // View der Index-Seite laden und ausgebe
    }
}

Wenn wir nun eine Anfrage an den Controller Home mit der Action Index bekommen, soll eine Instanz der Klasse ControllerHome erzeugt und die Funktion index() aufgerufen werden. Dank der Flexibilität von PHP lässt sich dies mit wenigen Zeilen Code automatisieren:

/**
 * Verarbeitet einen Controller-Action Request
 */
class RequestHandler {
    private $defaultController;
    private $defaultAction;

    /**
     * @param $defaultController    Standardcontroller der aufgerufen werden soll, wenn keiner übergeben wurde
     * @param $defaultAction        Standardaction des Standardcontrollers
     */
    public function __construct( $defaultController, $defaultAction ) {
        $this->defaultController = $defaultController;
        $this->defaultAction = $defaultAction;
    }

    /**
     * Verarbeitet eine eingehende CA-Anfrage
     * @param $get      HTTP-GET Array
     * @param $post     HTTP-POST Array
     */
    public function handleRequest( $get, $post ) {
        // POST und GET zusammenführen
        $request = array_merge( $get, $post );
        // Prüfen ob Controller und Action gesetzt sind, ansonsten Standardwerte verwenden
        $controller = 'Controller' . ( isset( $request['controller'] ) ? $request['controller'] : $this->defaultController );
        $action = isset( $request['action'] ) ? $request['action'] : $this->defaultAction;
        if( class_exists( $controller ) ) {
            $controllerInstance = new $controller( $request );
            // Action aufrufen, sofern diese existiert
            if( method_exists( $controllerInstance, $action ) ) {
                $controllerInstance->{$action}();
            }
        }
    }
}

In der index.php kann dies wie folgt eingebaut werden:

define( 'BASE_DIR', __DIR__ );
// RequestHandler laden, der die CA-Anfrage bearbeitet
$requestHandler = new RequestHandler( 'home', 'index' );
$requestHandler->handleRequest( $_GET, $_POST );

Ein Aufruf von index.php?controller=home&action=index würde die Klasse ControllerHome, also unseren Home-Controller, instanziieren und die Funktion index() aufrufen. Funktionieren wird der Code jedoch in dieser Form noch nicht: PHP wird sich beklagen, dass die Klasse ControllerHome nicht existiert – Zu recht, wurde die Datei Controller/ControllerHome nicht per include eingebunden. Das wurde bewusst übersehen, weil wir dieses Problem im nächsten Schritt ein für alle Mal lösen.

Mit Autoload geht alles besser

Die Zeiten von manuellen Includes sind vorbei: Seit Version 5.1 bietet PHP die Möglichkeit, Klassen automatisch einzubinden, wenn diese benötigt werden. Warum sollte man das nur für Controller nutzen, und nicht gleich für andere benötigte Dateien wie etwa die Views ebenfalls? Zum Einsatz kommt daher die bereits in einem anderen Beitrag vorgestellte AutoLoad-Klasse. Mit ihrer Hilfe können PHP-Dateien aus verschiedenen Ordnern durch die Definition von Regeln geladen werden:

define( 'BASE_DIR', __DIR__ );
define( 'INCLUDE_DIR', BASE_DIR . '/Include' );
// AutoLoad initialisieren
require_once INCLUDE_DIR . '/AutoLoader.php';
$autoLoader = new Autoloader();
// Klassen im Includes-Ordner direkt anhand ihres Namens ohne Prefix automatisch einbinden
$autoLoader->addLoadRule( '', '.php', INCLUDE_DIR );
// Controller automatisiert laden (Schema: Controller${NAME}.php)
$autoLoader->addLoadRule( '', '.php', BASE_DIR . '/Controller' );
$autoLoader->startWork();

In diesem Fall gibt es zwei Regeln: Die erste lädt alle Controller. Da diese bereits das Controller-Prefix im Klassennamen erhalten (ControllerHome), ist hier keine Angabe eines Präfixes nötig. Die Klasse ControllerHome.php wird aus Controller/ControllerHome.php geladen.

Die andere Regel gilt Allgemein für Klassen im Includes-Ordner. Auch hier sind Klassenname und Dateiname identisch: Eine Klasse namens User würde beispielsweise im Pfad Includes/User.php gespeichert werden. Der Scan verläuft rekursiv, somit werden Unterordner ebenfalls berücksichtigt. Die Klasse User würde also beispielsweise auch im Pfad Includes/User/User.php gefunden werden.

Nun können wir mit Klassen arbeiten, ohne diese Laden zu müssen – das erledigt der AutoLoader für uns. Der Request-Handler ist somit einsatzbereit. Er muss allerdings nach dem AutoLoader initialisiert werden, damit seine Klasse automatisch geladen wird:

// [...]
$autoLoader->startWork();

// RequestHandler laden, der die CA-Anfrage bearbeitet
$requestHandler = new RequestHandler( 'Home', 'Index' );
$requestHandler->handleRequest( $_GET, $_POST );

Beim Instanziieren des RequestHandlers wird eine Standardroute angegeben, die geladen werden soll, wenn weder Controller noch Action angegeben wurden – Also bei einem Aufruf der Seite ohne Parameter. In diesem Fall wäre das der Controller Home mit der Action Index. Groß/Kleinschreibung spielt auch hier keine Rolle, da Actions automatisch in Kleinbuchstaben umgewandelt werden.

Es werde View: Die Ansichten

Funktionsfähig ist die Anwendung damit jedoch immer noch nicht, weil es nichts anzuzeigen gibt: Die Views werden nicht geladen. Und selbst wenn würde das nichts bringen, weil wir noch gar keine haben. Auch das Laden der View lässt sich weitestgehendst automatisieren. Wir gehen davon aus, dass jede Action für gewöhnlich eine eigene View hat und diese gemäß Namenskonvention gleich heißt wie die Action des Controllers. Da jede Ansicht aus einem Controller heraus abgerufen werden muss, macht es Sinn diese Funktionalität in einer Basisklasse unterzubringen:

/**
 * Stellt Funktionen global für alle Controller zur Verfügung
 */
class BaseController {
    protected $request;
    // Vorangestellte Bezeichnung im Dateinamen von Controllern
    private $controllerPrefix = 'Controller';

    /**
     * Initialisiert den Basis-Controller
     * @param $request  Ein Array von GET- und/oder POST-Variablen
     */
    public function __construct( $request ) {
        $this->request = $request;
    }

    /**
     * Läd eine View aus der Action des Controllers heraus, sofern sie existiert
     * @param string $name  Name der zu ladenden View - Wenn dieser Parameter nicht angegeben wird, sucht die Funktion nach dem Namen der Action im Ordner des Controllers
     */
    public function view( $name = '') {
        // Liefert Informationen über die Klasse und Methode, von der aus diese Funktion aufgerufen wurde
        $callerClassInfo = debug_backtrace()[1];
        // Der Name des Controllers muss um das Prefix bereinigt werden: ControllerHome => Home
        $plainControllerName = substr( $callerClassInfo['class'], strlen( $this->controllerPrefix ) );
        // Der Unterordner für die Views ist identisch mit dem Namen des Controllers ohne Prefix
        $viewPath = VIEW_DIR . '/' . $plainControllerName;
        // Namen der aufrufenden Methode als Name der View nutzen, wenn keiner übergeben wurde
        if( strlen( $name ) == 0) {
            $name = $callerClassInfo['function'];
        }
        // View laden
        require_once INCLUDE_DIR . '/FunctionsView.php';
        loadView($name, $viewPath);
    }
}

Das eigentliche Einbinden der View wird allerdings in eine klassenlose Datei namens FunctionsView.php ausgelagert. Denn in den Views selbst müssen weitere Views eingebunden werden können – Etwa um den Header oder Footer zu laden, diese Views sollen Global in allen anderen eingebunden werden können. Die Funktion loadView() wird daher ohne Klassenzuordnung in FunctionsView.php ausgelagert:

/**
 * Läd eine View
 * @param        $controllerName    Name des Controllers bzw. der View
 * @param string $folder            Pfad des Ordners, in dem sich die View befindet (Standardmäßig das Root-Verzeichnis der Views)
 */
function loadView( $controllerName, $folder = VIEW_DIR ) {
    // Mit der PathInfo werden Pfadangaben relativ zum View-Verzeichnis korrekt an den Ordner angehängt wie z.B. Global/Header
    $pathInfo = pathinfo( $controllerName );
    if( $pathInfo['dirname'] != '.' ) {
        $folder .= '/' . $pathInfo['dirname'];
        $controllerName = $pathInfo['filename'];
    }
    // View laden und einbinden
    $fullPath = AutoLoader::searchFile($folder, 'View' . $controllerName . '.php', false);
    if( false !== $fullPath) {
        require_once $fullPath;
    }
}

Doch zurück zu unserem BaseController: Die Funktion view() kann parameterlos aufgerufen werden. Dann läd sie automatisch die View, welche sich im Unterordner mit dem Namen des Controllers befinden muss und gleich wie die Action heißt. Die View zur Action Index im Controller Home ist somit im Verzeichnis View/Home/ViewIndex.php zu finden.

Zum Aufruf der passenden View genügt somit eine Zeile Code:

class ControllerHome extends BaseController {
    public function __construct( $request ) {
        parent::__construct( $request );
    }

    public function index() {
        return $this->view();
    }
}

Natürlich muss diese View nun angelegt werden, also im Pfad View/Home/ViewIndex.php. Wie oben bereits schon erwähnt werden zumindest Footer und Header in der Regel zentral als eigene Views definiert und in die Seiten selbst lediglich eingebunden – Das vermeidet unnötige Redundanz. Im Beispiel hier werden 3 Templates erstellt:

Global/ViewFooter.php
Global/ViewHeader.php
Global/ViewNavigation.php

Über die Funktion loadView() können diese in der View der Seite (hier ViewIndex.php) eingebunden werden:

<?php loadView( 'Global/Header'); ?>

<body>
    <?php loadView('Global/Navigation'); ?>

    <div class="container">
        <h1>Willkommen!</h1>
        <div style="font-size: 16px">
            Dies ist eine Beispiel-Seite zur Demonstration einer grundlegenden PHP-MVC Webanwendung ohne externes PHP-Framework.
        </div>
    </div>
</body>

<?php loadView( 'Global/Footer' ); ?>

Wie man sieht werden alle Pfade relativ zum View-Ordner angegeben und erfordern keine Dateierweiterung, das übernimmt alles die loadView() Funktion. Und schon haben wir eine sehr einfache MVC-Anwendung in PHP entwickelt. Es fehlen lediglich Models, was jedoch schlicht und einfach an den fehlenden Daten liegt.

Eine Template-Engine?

Nun haben wir eine MVC-Anwendung in PHP bzw. das Grundgerüst dafür, aber keine Template-Engine. Oder etwa doch? Ich mag zwar grundsätzlich das an C angelehnte Syntax mit den geschweiften Klammern. Aber zugegeben: In einer View sind Konstrukte wie das folgende Beispiel wirklich nicht das wahre.

<?php if( $user->isLoggedIn() ) { ?>
    Hallo <?=$user->getUsername()?>!
<?php } else { ?>
    Du bist nicht angemeldet!
<?php } ?>

Es ist weder schön zu lesen noch zu schreiben. Das haben auch die PHP-Entwickler vor längerem bereits bemerkt und daher bereits in PHP4 das sogenannte Alternative Syntax für Kontrollstrukturen integriert. Es soll diesem Wirrwar von Klammern und PHP-Tags Einhalt gebieten. Das obige Beispiel sieht demnach wie folgt aus:

<?php if( $user->isLoggedIn() ): ?>
    Hallo <?=$user->getUsername()?>!
<?php else: ?>
    Du bist nicht angemeldet!
<?php endif; ?>

Zwar etwas mehr Tipparbeit, aber trotzdem deutlich angenehmer und übersichtlicher. In der ersten Bedingung sieht man außerdem eine weitere Neuerung: Der sogenannte Short Open-Tag ist ab PHP 5.4 standardmäßig aktiviert. Zuvor musste er explizit über die php.ini aktiviert werden, was jedoch kaum jemand tat. Daher ist es nicht verwunderlich, dass er praktisch nicht verwendet wurde. Immerhin hatte jeder Entwickler zu befürchten, dass seine Webanwendung dann auf einem Großteil der PHP-Webhoster ohne weiteres nicht funktionieren würden.

Das ist schade, denn bei der Ausgabe von Variablen ist <?= wirklich praktisch: Man spart sich nicht nur den vollen PHP-Tag der normal aus <?php besteht sondern auch das echo zur Ausgabe sowie das Semikolon am Ende. Diese Entwicklungen machen deutlich: PHP wurde eigentlich als Sprache für Templates entwickelt und wird auch in der Richtung weiterentwickelt. Zugegeben, mit der Template-Engine Smarty ließe sich der obige Block noch weiter verkürzen:

{if $user->isLoggedIn()}
    Hallo {$user.getUsername()}!
{else}
    Du bist nicht angemeldet!
{/if}

Die Frage welche man sich jedoch stellen sollte lautet: Sind es die paar gesparten Zeichen wert, eine doch recht umfangreiche Template-Engine einzusetzen? Zumindest bei kleineren Projekten dürfte nicht selten festzustellen sein, dass die Boardmittel von PHP eigentlich völlig ausreichen. Das verbessert nicht nur die Performance, sondern reduziert auch die Anzahl der Abhängigkeiten.

Fazit

Um kleinere PHP-Anwendungen nach dem MVC-Muster zu entwickeln, braucht es definitiv keine mächtigen externen Frameworks. Im Einzelfall kann das natürlich Sinn machen, aber meist sind diese zu überladen. Was PHP an Board hat reicht völlig aus. Dazu unser selbst entwickeltes Micro-Framework, dass in der hier vorgestellten minimalen Form keine 200 Codezeilen umfasst.

Das Beispiel aus diesem Artikel wurde zu einem Beispielprojekt mit 3 . Im gleichen Zuge kamen auch ein paar Hilfsfunktionen dazu, etwa zur Einbettung von JavaScript-Dateien oder Links in Templates. Darauf wurde in diesem Artikel nicht weiter eingegangen, da es mit dem Kernthema nichts zutun hat. Wer möchte kann sich das Beispielprojekt im folgenden herunterladen. Es benötigt lediglich PHP 5.4 oder höher und läuft problemlos in Entwicklungsumgebungen wie etwa das bekannte Paket XAMPP.

PHP-MVC-Example herunterladen (282 KB)

Leave a Reply