freeload4u
 

PHP und das Model-View-Controller Prinzip

Größere PHP - Projekte so gehts... Ziel ist es, auch bei sehr umfangreichen Web-Projekten die Übersicht zu behalten und eine Anwendung zu entwickeln die gut wartbar ist und sich leicht erweitern lässt.


Ausgangspunkt

  1. Apache-Webserver >= 2.4.25
  2. MySQL - Server >= 5.7
  3. PHP >= 7.0

Ziel

Vorraussetzungen


Download MVC - Beispiel

Einführung
Model View Controller (MVC, englisch für Modell-Präsentation-Steuerung) ist ein Muster zur Trennung von Software in die drei Komponenten Datenmodell (engl. model), Präsentation (engl. view) und Programmsteuerung (engl. controller). Das Muster kann sowohl als Architekturmuster, als auch als Entwurfsmuster eingesetzt werden. Ziel des Musters ist ein flexibler Programmentwurf, der eine spätere Änderung oder Erweiterung erleichtert und eine Wiederverwendbarkeit der einzelnen Komponenten ermöglicht.

1. Apache Konfiguration (mod_rewrite, vhost)

Als erstes aktivieren wir das Apache - Modul "rewrite" in dem folgendes als root ausgeführt wird:
        
            a2enmod rewrite
        
    
        
            vi /etc/apache2/sites-available/example.com.conf
        
    

Die Vhost in unserem Beispiel sollte so aussehen. Ich habe einfach mal eine Subdomain erstellt um alles seperat testen zu können.
        
            <VirtualHost *:80>
                ServerName mvc.example.com
                DocumentRoot /var/www/mvc.example.com/
                    <Directory /var/www/mvc.example.com/>
                        AllowOverride All
                        RewriteEngine on

                        #Rules
                        RewriteRule    ^([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ index.php?controller=$1&function=$2&prams=$3    [NC,L]
                        RewriteRule    ^([A-Za-z0-9-]+)/([A-Za-z0-9-]+)/?$ index.php?controller=$1&function=$2    [NC,L]
                        RewriteRule    ^([A-Za-z0-9-]+)/?$ index.php?controller=$1    [NC,L]

                    </Directory>
            </VirtualHost>
        
    
Es besteht auch die Möglichkeit, die Rules in eine .htaccess zu speichern.

Erklärung
Unsere Web-App bauen wir folgendermaßen auf:
http://mvc.example.com/param1/
http://mvc.example.com/index.php?controller=param1

http://mvc.example.com/param1/param2/
http://mvc.example.com/index.php?controller=param1&function=param2

http://mvc.example.com/param1/param2/other/
http://mvc.example.com/index.php?controller=param1&function=param2&prams=other


Beispiel
http://mvc.example.com/contact/ wird zu
http://mvc.example.com/index.php?controller=contact

Später wird der contactController geladen und die dazugehörige view angezeigt.

Apache reloaden nicht vergessen:
        
            /etc/init.d/apache2 reload
        
    

2. Ordnerstruktur


Diese Struktur brauchen wir uns jetzt nicht manuell erstellen, sondern können das komplette Projekt hier runterladen.

3. Controller-Klassen

./classes/controllers/controller.php
        
            <?php

            class Controller {

                private $request        = null;
                private $view           = null;

                // Vererbt in z.B. contactController, indexController, wird dort gesetzt...
                protected $innerView    = null;

                public function __construct($request)
                {
                    // Übergabe aller Requests ($_POST & $_GET)
                    $this->request      = $request;
                }

                /**
                *
                * Prüfen ob die Methode existiert die über den function Parameter aufgerufen werden soll.
                * Beispiel: http://mvc.example.com?controller=contact&function=sendFrm
                * http://mvc.example.com/contact/sendFrm
                *
                * @param $class
                * @param $method
                * @return bool
                */
                public function isMethodExists($class, $method)
                {
                    if (method_exists(get_class($class), $method))
                    {
                        // Zur Sicherheit, sollte die Methode nur aufgerufen werden können, wenn sie auch public ist.
                        $reflection = new ReflectionMethod($class, $method);
                        if ($reflection->isPublic())
                        {
                            return true;
                        }else
                        {
                            exit("Methode $method ist nicht public in ".get_class($class));
                        }

                    }else
                    {
                        exit("Methode $method existiert nicht in ".get_class($class));
                    }

                }

                /**
                * Ausgabe des kompletten Templates inklusive des inneren View
                */
                public function display()
                {
                    // View erstellen
                    $this->view = new View();

                    // Innere View hinzufügen (assign)
                    $this->view->assign('content', $this->innerView->loadTemplate());

                    // Äußere View (main.php)
                    $this->view->setTemplate('main');

                    // Rückgabe komplettes Template
                    return $this->view->loadTemplate();
                }
            }


            ?>

        
    

./classes/controllers/contactController.php
        
            <?php
            class contactController extends Controller{

                private $request    = null;


                public function __construct($request)
                {
                    // alle $_GET & $_POST Parameter setzen
                    $this->request  = $request;

                    // neue innere View (in Controller)
                    $this->innerView = new View();

                    parent::__construct($request);

                    /**
                    * Falls der Parameter function (Methode) gesetzt ist, wird versucht diese aufzurufen.
                    * ansonsten wird die Methode index() aufgerufen
                    *
                    * z.B. http://mvc.example.com/contact/ ohne weiteren Parameter führt dazu, dass index() aufgerufen wird
                    * z.B. http://mvc.example.com/contact/impressum ruft die Methode impressum() auf.
                    * oder als AjaxRequest an http://mvc.example.com/contact/sendFrm/ ruft die Methode sendFrm() auf.
                    */
                    if(isset($request['function']))
                    {
                        if($this->isMethodExists($this, $request['function']) && is_callable(array(get_class($this), $request['function'])))
                        {
                            call_user_func(array(get_class($this), $request['function']));
                        }
                    }else
                    {
                        $this->index();
                    }

                }


                public function index()
                {
                    // Daten assignen aus contactModel
                    $this->innerView->assign('email', contactModel::getData());

                    // Template setzen
                    $this->innerView->setTemplate('contact');
                }

                /**
                * Unterseite von contact
                * z.B. http://mvc.example.com/contact/impressum/
                *
                */
                public function impressum()
                {
                    // Template setzen
                    $this->innerView->setTemplate('impressum');
                }

                /**
                * Methode ohne Template z.B. für AjaxRequest
                */
                public function sendFrm()
                {
                    /* mail() */

                    echo "ajax test123";
                    print_r($this->request);

                    // keine weiteren Ausgaben (HTML)
                    exit();
                }


                private function test()
                {
                    return 123;
                }
            }
            ?>

        
    

4. Die View Klasse
./classes/view.php
        
            <?php


            class View{

                // Pfad zum Template
                private $path = 'templates';
                // Name des Templates, in dem Fall das Standardtemplate.
                private $template = 'main';

                /**
                 * Enthält die Variablen, die in das Template eingebettet
                 * werden sollen.
                 */
                private $_ = array();

                /**
                 * Ordnet eine Variable einem bestimmten Schlüssel zu.
                 *
                 * @param String $key Schlüssel
                 * @param String $value Variable
                 */
                public function assign($key, $value){
                    $this->_[$key] = $value;
                }


                /**
                 * Setzt den Namen des Templates.
                 *
                 * @param String $template Name des Templates.
                 */
                public function setTemplate($template = 'main'){
                    $this->template = $template;
                }

                /**
                 * Das Template-File laden und zurückgeben
                 *
                 * @param string $tpl Der Name des Template-Files (falls es nicht vorher
                 *                      über steTemplate() zugewiesen wurde).
                 * @return string Der Output des Templates.
                 */
                public function loadTemplate(){
                    $tpl = $this->template;
                    // Pfad zum Template erstellen & überprüfen ob das Template existiert.
                    $file = $this->path . DIRECTORY_SEPARATOR . $tpl . '.php';
                    $exists = file_exists($file);

                    if ($exists){
                        // Der Output des Scripts wird n einen Buffer gespeichert, d.h.
                        // nicht gleich ausgegeben.
                        ob_start();

                        // Das Template-File wird eingebunden und dessen Ausgabe in
                        // $output gespeichert.
                        include $file;
                        $output = ob_get_contents();
                        ob_end_clean();

                        // Output zurückgeben.
                        return $output;
                    }
                    else {
                        // Template-File existiert nicht-> Fehlermeldung.
                        return 'could not find template';
                    }
                }
            }


            ?>
        
    

5. Einstiegspunkt
./index.php
        
            <?php

            require_once("autoload.php");

            // alle Requests zusammenfügen und an die Controller-Klasse übergeben
            $request = array_merge($_GET, $_POST);

            // Parameter controller (z.B. contact)
            if(isset($request['controller']))
            {
                $controllerName = $request['controller']."Controller";

                /**
                * Sofern sich der $controllerName.php im Array($arrayFilesToLoad) befindet,
                * wird versucht eine Controller-Instanz zu erstellen.
                */
                if(in_array($controllerName.".php", $arrayFilesToLoad['file']))
                {
                    $controller = new $controllerName($request);
                }else
                {
                    exit("Fehler: kann $controllerName nicht laden...");
                }


            }else
            {
                $controller = new indexController($request);
            }

            // Ausgabe
            echo $controller->display();

            ?>
        
    

./autoload.php
        
            <?php

            require_once ("config/settings.php");
            require_once (CONTROLLER_PATH."controller.php");


            // Array mit den gefundenen Klassen bzw. files erstellen
            $arrayFilesToLoad['file'] = array();
            $arrayFilesToLoad['path'] = array();

            // Unterordner mit einbeziehen (CLASSES_PATH in settings.php)
            $fileinfos = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator(CLASSES_PATH)
            );

            // Ordner (Klassen) durchgehen
            foreach($fileinfos as $pathname => $fileinfo) {
                if (!$fileinfo->isFile()) continue;

                // Files (Klassen) in ein Array speichern
                array_push($arrayFilesToLoad['file'], $fileinfo->getFileName());
                array_push($arrayFilesToLoad['path'], $pathname);

                // Klasse einbinden (File)
                require_once $pathname;
            }


            ?>
        
    

Wird jetzt im Ordner ./classes/ eine neue Datei angelegt, wird Sie automatisch included. Unterordner wie z.B. models oder controllers mit einbezogen.

./config/settings.php
        
            <?php
                error_reporting (E_ALL);
                ini_set ('display_errors', 'On');

                define("CONTROLLER_PATH", "classes/controllers/");
                define("CLASSES_PATH", "classes/");

                // MySQL connection
                define("DB_HOST", "127.0.0.1");
                define("DB_NAME", "datenbank");
                define("DB_PORT", "3306");
                define("DB_USER", "user1");
                define("DB_PASS", "pass123");
            ?>
        
    

6. Unsere Views
./templates/main.php
        
            <!DOCTYPE html>
            <html lang="de">
                <head>
                    <title>Test MVC</title>
                    <base href="http://mvc.freeload4u.de/">
                </head>
                <body>
                    Ich bin die main.php
                    <a href="/">Home</a>
                    <a href="/contact/">Kontakt</a>
                    <a href="/contact/impressum">Impressum</a>
                    <br>
                    <?= $this->_['content']; ?>
                    <br><br>
                    footer:<br>
                    footer-text

                    <script src="/includes/js/load.js"></script>
                </body>
            </html>
        
    
Hierbei handelt es sich um das äußere Template indem das innere Template eingebunden wird (<?= $this->_['content']; ?>).

Das <?= ?> ist ein sogenannter PHP-ShortTag es verkürzt das <?php echo $this->_['content'] ?>. Um ShortTags nutzen zu können muss es in der php.ini aktiviert werden.

Die php.ini befindet sich standardmäßig in /etc/php/7.0/apache2/
        
            vi /etc/php/7.0/apache2/php.ini
        
    

In Zeile 202 finden wir nun
        
            short_open_tag = Off
        
    

Diese stellen wir auf On und speichern anschließend. Danach sollte der Apache einmal neugestartet werden.
        
            /etc/init.d/apache2 restart
        
    


Es geht nun weiter mit der inneren View. Beispiel:
./templates/contact.php
        
            <strong>Ich bin Contact</strong>
            <button class="btnClickTest">send frm</button>
            <br>
            Data: <?= $this->_['email']; ?>
        
    


7. jQuery (Ajax-Request) zum Controller
./includes/app.js
        
            $(function()
            {
                $(".btnClickTest").click(function ()
                {
                    // Daten
                    var d = {"var1": "asdf", "var2": "222asdf"};
                    $.ajax({
                        type: 'POST',
                        url: '/contact/sendFrm',
                        data: d,
                        success: function(data) {
                            alert(data);
                        }
                    });
                });

            });
        
    

Es wird im contactController die Methode sendFrm() aufgerufen. Die gesendeten Daten (var1 & var2) können dort nun verarbeitet werden.

8. Models
./classes/models/contactModel.php
        
            <?php


            class contactModel
            {
                public static function insertData()
                {
                    $sql = "INSERT INTO table_test SET field1 = :field1, field2 = :field2";
                    $params = array(
                        'field1'     => "text123",
                        'field2'     => 123
                    );

                    return DB::exe($sql, $params);

                }


                public static function getData()
                {
                    /*
                    $sql = "SELECT email FROM table_test WHERE id = :id LIMIT 1";
                    $params = array(
                        'id'     => 123
                    );

                    $res = DB::exe($sql, $params);

                    return $res[0]['email'];
                    */

                    return "test@mail.com";

                }

                public static function deleteData()
                {
                    $sql = "DELETE FROM table_test WHERE id = :id LIMIT 1";
                    $params = array(
                        'id'     => 123
                    );

                    return DB::exe($sql, $params);

                }
            }

            ?>
        
    


9. Die fertige DB Klasse
./classes/models/db.php
        
            <?php


            class DB
            {
                protected static
                    $dbh;

                public static function connect($host, $username, $password, $database=null)
                {
                    try{
                        $database = ($database) ? ';charset=utf8;port='.DB_PORT.';dbname=' . $database : '';
                        self::$dbh = new PDO('mysql:host=' . $host . $database, $username, $password);
                        return;
                    }catch(PDOException $e){
                        throw new Exception($e->getMessage());
                    }
                }
                public static function close()
                {
                    self::$dbh = null;
                }
                public static function exe($sql, $para=null)
                {
                    if(!self::$dbh)
                    {
                        self::connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
                    }

                    $para_copy = $para;
                    $stmt = self::$dbh->prepare($sql);
                    $bind_para = ($para !== null
                        and (strpos($sql, ' LIMIT :') !== false or strpos($sql, ' limit :') !== false)
                    ) ? true : false;

                    if($bind_para and is_array($para)){
                        foreach($para as $key => &$val){
                            if(is_string($val)){
                                $stmt->bindParam($key, $val, PDO::PARAM_STR);
                            }
                            elseif(is_bool($val)){
                                $stmt->bindParam($key, $val, PDO::PARAM_BOOL);
                            }
                            elseif(is_null($val)){
                                $stmt->bindParam($key, $val, PDO::PARAM_NULL);
                            }
                            elseif(is_numeric($val)){
                                $stmt->bindParam($key, $val, PDO::PARAM_INT);
                            }
                            else{ // PDO::PARAM_FLOAT does not exist. handle float as string
                                $stmt->bindParam($key, (string)$val, PDO::PARAM_STR);
                            }
                        }
                        $para = null;
                    }

                    if(!$stmt->execute($para)){
                        $err_info   = $stmt->errorInfo();
                        $sql_state  = $err_info[0];
                        $ecode      = $err_info[1];
                        $emsg       = $err_info[2];

                        $sql_state  = '(SQLSTATE: ' . $sql_state . ')';
                        $ecode      = '(eCode: ' . $ecode . ')';
                        $emsg       = 'eMessage: ' . $emsg;

                        $error = $sql_state . ' ' . $emsg . ' ' . $ecode;

                        $sql = preg_replace('/\s+/', ' ', $sql);

                        $para_sring = '';
                        if($para_copy){
                            foreach($para_copy as $k => $v){
                                $para_sring .= ($para_sring === '') ? '' : '; ';
                                $para_sring .= ((strpos($k, ':') !== false) ? '' : ':') . $k . ' => ' . $v;
                            }
                        }

                        $error .= 'query: ' . $sql . ' para: ' . $para_sring;
                        throw new Exception($error);
                    }
                    $result = null;
                    while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
                        if($result === null){
                            $result = array();
                        }
                        $result[] = $row;
                    }
                    $stmt = null;
                    return $result;
                }
                public static function lastInsertId()
                {
                    return self::$dbh->lastInsertId();
                }
            }

            ?>
        
     

Wir sind nun bereit für ein größeres PHP - Projekt. Der fertige MVC kann hier runtergeladen werden.

10. eine neue Seite anlegen (unbedingt auf Groß- und Kleinschreibung achten)
  1. Wir kopieren einfach die indexController.php in der sich bisher nur die index() Methode befindet. In z.B. pageXController.php (pageX steht für den neuen Seitennamen).
  2. Nachdem wir die Datei kopiert haben passen wir lediglich noch den Klassennamen an: class indexController extends Controller { wird zu class pageXController extends Controller {
  3. Dann legen wir im Ordner templates eine neue view (Seite) an. Z.B. pageX.php
  4. Zuletzt setzen wir in in der index() Methode unseres neuen Controllers noch die neue View: $this->innerView->setTemplate('pageX');

Wenn wir jetzt http://mvc.example.com/pageX aufrufen wird die pageX.php als innere View angezeigt.

Falls der neue Controller Daten aus unserer Datenbank verarbeiten soll, muss natürlich noch ein neues Model in (.classes/models/) angelegt werden. Dazu kopieren wir uns wieder einfach contactModel.php in pageXModel.php und passen auch dort den Klassennamen an.

fertig :-)