Eine OXID eShop Admin-Seite erstellen

OXID eShop

Dieser Beitrag ist eine überarbeitete Fassung eines alten Tutorials. Sie nutzt noch stärker die dynamische Moduleinbindung von OXID CE 4.6 / 4.7, so dass der Endnutzer das Modul später mit einem Klick installieren kann.

Dieses Tutorial soll dabei helfen ein Backend-Modul in Form einer Unterseite im OXID-Administrationsbereich zu erstellen. Dabei werden schwerpunktartig folgende Bereiche behandelt:

  • Wie lege ich einen neuen Menüpunkt im OXID-Menü an?
  • Wie erstelle ich eine Seite für das OXID-Backend?
  • Wie greife ich auf Shopinformationen zu (in diesem Fall konkret auf Bestelldaten)?

Bestellstatistik

Beispielhaft werde ich eine kleine Statistik-Seite erstellen, die graphisch die Anzahl der Bestellungen über die letzten Monate anzeigt. Dabei geht es mehr darum, zu zeigen, wie man mit OXID auf die Shop-Daten zugreift, und diese der Smarty-Template-Engine zur Verfügung stellt, als um die Statistik selbst – ist ja auch schöner als die einhunderdste “Hello world!”-Seite zu programmieren.

Quelltexte und Libraries zum Tutorial

Alle für dieses Tutorial benötigten Sourcen kann man sich [hier, Link leider nicht mehr vorhanden] herunterladen. In dem Download sind auch die benötigten jqplot-Bibliotheken enthalten.

Für wen ist das interessant?

Dieses Tutorial richtet sich an alle PHP-Entwickler, die einen schnellen Einstieg in die grundlegende OXID-Programmierung suchen. In kleinem Maße wird in diesem Tutorial auch mit Smarty gearbeitet, diese Blöcke sind aber ziemlich selbsterklärend und fordern kein umfangreiches Wissen über die Entwicklung von Smarty-Templates. Zusätzlich werden jQuery, CSS und XML minimal tangiert, es schadet also nicht zu wissen, was das ist.

Voraussetzung für die Entwicklung ist ein aufgesetzter OXID Shop. In dem Tutorial arbeite ich mit der Version CE 4.7.x. Für PE und EE dürfte es aber ganz genau so funktionieren. In den älteren OXID-Versionen ist die Einbindung von OXID-Modulen noch etwas anders, alles andere ist (was dieses Tutorial betrifft) gleich. Wer also auf einem älteren System entwickeln will oder muss, der sollte einfach den Abschnitt über die Modul-Einbindung weglassen und sich ein Tutorial über die Einbindung in OXID 4.5.x durchlesen.

Die OXID-Struktur

OXID folgt schematisch relativ strikt dem Model-View-Controller-Prinzip. Das bedeutet, dass die Darstellung getrennt ist von Daten und Shoplogik.

Die Shopinformationen liegen in der Datenbank und werden von OXID über Wrapper-Klassen angesteuert. Diese Wrapper-Klassen erkennt man in der Regel in OXID an dem Präfix “ox” plus Datenbank-Tabelle, auf die sich das Objekt bezieht. oxarticle stellt also einen Zugriff auf die Artikelinformationen des Shops dar, oxuser auf Benutzerinformationen und – für unser Bestellstatistik-Tool wichtig – oxorder auf die Bestellinformationen. Über oxorder können wir also eine bestimmte Bestellung laden und seine Informationen abfragen. Ab OXID CE 4.7 wird in der Dateistruktur außerdem unterschieden zwischen allgemeinen Hilfsklassen, die sich im Verzeichnis /core/ befinden (dazu gehört beispielsweise die Klasse oxList) und solchen Klassen, die einen konkreten Bezug zu E-Commerce also zum Online-Shopping haben (wie etwa oxArticle), diese findet man im Verzeichnis /application/models.

Hinweis: Die Benutzung der Wrapper-Klassen ist immer dann sinnvoll, wenn die Performance eine untergeordnete Rolle spielt. Braucht man beispielsweise auf einer Artikel-Detailsseite im Shop Informationen über einen (oder vielleicht ein Dutzend Artikel) empfiehlt sich der Übersichtlichkeit halber auf jeden Fall die Nutzung der Wrapper-Klasse oxArticle. Beim Iterieren durch tausende Datensätze hingegen, ist das Laden und Befüllen der Wrapper-Klassen der absolute Performance-GAU. Daher werden wir die Statistik über einen direkten Datenbank-Query aufstellen und auf die Komfort-Klassen an der Stelle verzichten.

Die Darstellung der Informationen wird in OXID mithilfe von Smarty-Templates realisiert. Diese befinden sich für den Admin-Bereich des Shops im Ordner /application/views/admin/tpl/. Smarty-Templates sind ähnlich wie PHP-Dateien Skripte, die dynamisch Webseiten generieren. Einige Features sind in Smarty vereinfacht, außerdem bietet Smarty komfortable Möglichkeiten Blöcke zu definieren und so die Wiederverwendbarkeit von Sourcen zu erhöhen.

In OXID hat (beinahe) jedes Smarty-Template eine Controller-Klasse. Diese befinden sich im Ordner /application/controllers/. In älteren OXID-Versionen befanden sich diese Klassen irreführenderweise in einem Ordner “views” und wurden (zumindest von mir) View-Klassen genannt. Der Begriff Controller-Klasse ist allerdings im MVC-Modell deutlich passender. Diese Klassen stellen die Verbindung zwischen der Darstellung und dem Modell her, liefern also Datenbankinformationen für die Benutzeroberfläche und verarbeiten Aktionen auf der Benutzeroberfläche und geben sie an die Shoplogik weiter. Besonders wichtig ist dabei die render()-Methode der Controller-Klasse, die immer vor dem Auswerten der Template-Dateien ausgeführt wird.

Nun zur Praxis

Genug Theorie für den Anfang, jetzt wollen wir was sehen!

Die Controller-Klasse

Weil wir auf unserer Statistikseite den Bestellverlauf über die letzten Monate darstellen wollen, müssen wir diese ersteinmal in unserer Controller-Klasse zur Verfügung stellen, so dass sie im Smarty-Template genutzt werden kann. Eine schlechte Idee wäre jetzt das ganze einfach in den OXID eigenen Ordnern zu machen, da wir dadurch die Update-Fähigkeit des Shops verlieren würden und unser Modul der Welt nicht zum dynamisch dazupappen zu ihrem OXID Shop zur Verfügung stellen könnten. Wir legen es also als Modul an. Dafür hat OXID den Ordner /modules/. Wir erzeugen also einen Ordner /modules/eins_order_stats und legen darin eine Datei controllers/eins_order_stats.php an (es ist immer gut einem Modul ein Präfix voranzustellen, um die Eindeutigkeit sicherzustellen, sollte das Modul mit anderen Modulen kombiniert werden). Unsere Controller-Klasse sieht folgendermaßen aus:

<?php

/*
 *  Developed by einscommerce.com
 *  Author: Lukas Dierks <lukas.dierks at einscommerce.com>
 *  Date: Dec 30, 2013
 */

class eins_order_stats extends oxAdminView {
    /**
     * The render function is invoked before the smarty
     * template is evaluated.
     * 
     * @return string The template to be shown.
     */
    public function render() {
        parent::render();
        
        // Get a connection to the database
        $oDb = oxDb::getDb();
        // Query all order dates
        $oRs = $oDb->execute("SELECT oxorderdate FROM oxorder");
        
        // Count every order in the statistics
        while(!$oRs->EOF) {
            $sDateTime = $oRs->fields[0];
            $iTimeStamp = strtotime($sDateTime);
            $iYear = date("Y", $iTimeStamp);
            $iMonth = date("m", $iTimeStamp);
            $iDay = date("d", $iTimeStamp);
            
            // The month of the order in respect is increased by one
            $aStats[$iYear][$iMonth]++;
            $oRs->moveNext();
        }
        
        // Provide the statistics array to the smarty template engine
        $this->_aViewData['aStats'] = $aStats;
        
        // Return the template that should be shown
        return 'eins_order_stats.tpl';
    }
}
?>

Die eins_order_stats-Klasse erbt von oxAdminView, so dass sie die nötige Funktionalität bietet um im OXID-Backend dargestellt werden zu können. Wir überschreiben die render()-Methode. Diese Methode wird vor der Auswertung der Template-Datei ausgeführt und gibt den Namen der Template-Datei zurück, die dargestellt werden soll. Sie ist also dafür geeignet Daten vor der Darstellung vorzubereiten – in unserem Fall die Statistik über die Bestellungen.

Hinweis: An dieser Stelle wurde in früheren Versionen dieses Tutorials eine oxList erstellt und mit dem Bestellinformationen befüllt. Aus Performance-Gründen habe ich diesen Schritt durch ein direktes Auslesen der Datenbank ersetzt.

Ich erzeuge mithilfe von oxDb eine Query an die Datenbank und lasse mir das Datum jeder Bestellung zurückgeben. Im Array $aStats zähle ich die einzelnen Ergebnisse.

Schließlich stellen wir das Ergebnis der Smarty-Engine zur Verfügung, indem wir es in das geerbte _aViewData-Array schreiben.

Damit sind wir auf der Controller-Ebene fertig.

Das Template

Jetzt müssen wir uns um das Template kümmern. Im Verzeichnis /modules/eins_order_stats/views/admin/tpl/ legen wir die Datei eins_order_stats.tpl an:

<!DOCTYPE html>
<html>
    <head>
        [{* Load all style sheets and jQuery libraries *}]
        <link rel="stylesheet" type="text/css" href="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/css/eins_order_stats.css" />
        <link rel="stylesheet" type="text/css" href="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/css/jquery.jqplot.css" />
        <link rel="stylesheet" href="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/css/morris.css">
        
        <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>        
        <script language="javascript" type="text/javascript" src="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/js/jqplot/jquery.jqplot.min.js"></script>
        <script src="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/js/morris_0-4-3/morris.js"></script>
        <script src="[{$oViewConf->getBaseDir()}]modules/eins_order_stats/views/admin/src/js/raphael/raphael-min.js"></script>
    </head>
    <body>
        [{* Iterate over every month of the provided statistics array *}]
        [{foreach from=$aStats key="iYear" item="aYear"}]
            <div class="order_graph">
                <h1>[{$iYear}]</h1>
                [{* This empty div will be filled by jqPlot later *}]
                <div id="graph[{$iYear}]" class="order_graph"></div>
                </div>
                [{* For every month one chart template is included *}]
                [{include file="eins_order_stat_chart.tpl" year=$iYear stats=$aYear}]
        [{/foreach}]
    </body>
</html>

Für das Plotting der Daten habe ich mich für das jQuery-Toolset jqplot entschieden (http://www.jqplot.com). Die Sourcen kann man sich im Download-Bereich auf der Seite ziehen. Ich habe sie im Ordner /modules/eins_order_stats/views/admin/src/js/jqplot abgelegt. Außerdem braucht man für die Kalender-Darstellung die Bibliotheken von http://www.oesmith.co.uk/morris.js/ und http://raphaeljs.com/. Diese sind meinen Sourcen schon beigefügt.

In den ersten Zeilen von eins_order_stats.tpl werden diese jQuery-Bibliotheken und CSS-Dateien geladen. Daraufhin wird in der foreach-Schleife für jedes Jahr ein leerer div-Container angelegt, der später den Graphen des jeweiligen Jahres enthalten wird. Der Übersichtlichkeit halber gibt es für jeden Graphen dann ein eigenes Template eins_order_stat_chart.tpl, dass über ein include geladen wird:

<script type="text/javascript">
    [{* The jQuery script is generated with the provided data *}]
    var order_data = [
        [{foreach from=$stats item="iOrderAmount" key="key"}]

            { "period": "[{$iYear}]-[{$key}]",
                        "amountordered": [{$iOrderAmount}],
            },
        [{/foreach}]
    ];
    
    Morris.Line({
        [{* The element field tells jqPlot which empty
            div to look for to fill with the chart. 
            See the jqPlot documentation for more information *}]
        element: 'graph[{$year}]',
        data: order_data,
        xkey: 'period',
        ykeys: ['amountordered'],
        labels: ['[{oxmultilang ident="eins_order_stats_orders"}]']
    });
</script>

Der Chart wird dann mit einem Javascript mit den Bestelldaten befüllt.

Wichtig, um zu wissen, wie das funktioniert ist sich klar zu machen in welcher Reihenfolge das Skript ausgewertet wird. Zuerst wird die render()-Methode der Controller-Klasse ausgeführt. Diese stellt der Smarty-Template-Engine das Array $aStats zur Verfügung indem sie es in das _aViewData-Array schreibt. Danach wird die Smarty-Engine aktiv und erzeugt die HTML-Seite inklusive des Javascripts. Der Klient bekommt also das Javascript schon befüllt mit allen Punkten des Graphen. Klient-seitig wird dann das jqplot-Javascript ausgeführt, das nach dem angegebenen div-Container sucht und den Inhalt mit dem entsprechenden Chart füllt.

Eintrag ins Admin-Menü

Jetzt muss das Modul noch im Admin-Menü angezeigt werden. Dieses wird in der Datei /application/views/admin/menu.xml definiert. Um es modular zu erweitern lege ich im Verzeichnis modules/eins_order_stats/ eine Datei menu.xml an, die den Link zur neuen Admin-Seite unter eShop Admin > Statistiken anlegt:

<?xml version="1.0" encoding="ISO-8859-15"?>
<?xml version="1.0" encoding="ISO-8859-15"?>
<OX>
  <OXMENU id="NAVIGATION_ESHOPADMIN">
    <MAINMENU id="mxstat">
        <SUBMENU id="eins_order_stats" cl="eins_order_stats" rights="malladmin" />
    </MAINMENU>
  </OXMENU>
</OX>

OXID durchsucht alle Unterverzeichnisse des modules-Ordners nach menu.xml-Dateien und fügt sie in das Backend-Menü ein. Diese werden dann mit der menu.xml aus dem Verzeichnis /admin/ “zusammengemerged”, man legt also über die ids in <oxmenu> und <mainmenu> fest, wo der Eintrag mit reinrutscht, oder ob es ein neues Menü ist (wenn man eine neue id vergibt).

Außerdem erstelle ich eine neue Datei /modules/eins_order_stats/views/admin/de/eins_order_stats_lang.php sowie eine Datei /modules/eins_order_stats/views/admin/en/eins_order_stats_lang.php.

OXID sucht in jedem (aktiven) Modul nach PHP-Dateien im Ordner [MODUL_ORDNER]/views/admin/[LÄNDER_KÜRZEL] (beziehungsweise für das Frontend im Ordner [MODUL_ORDNER]/views/[LÄNDER_KÜRZEL]. Durch das Erstellen dieser Multi-Lang-Dateien ist sichergestellt, dass für jede angebotene Sprache eine Übersetzung vorhanden ist.

<?php

$sLangName = "Deutsch";
// -------------------------------
// RESOURCE IDENTITFIER = STRING
// -------------------------------
$aLang = array(
    'charset' => 'UTF-8',
    'eins_order_stats' => 'einscommerce Bestellstatistik',
    'eins_order_stats_orders' => 'Bestellungen'
    	
);

Unter Umständen kann es jetzt nötig sein, einmal per FTP den tmp-Ordner von OXID zu leeren, da die Sprach-Brocken von OXID gecachet werden und die neuen Einträge erst danach übernommen werden.

Fertig oder?

Der stolze Programmierer klickt nun mit feuchten Augen auf den neu erstellten Link. Und? Aua! Das liegt daran, dass wir unser Modul noch nicht aktiviert haben und OXID damit gesagt haben, welche Controller-Klassen und Templates es wie laden soll.

Einbinden des Moduls

Damit OXID (ab Version 4.6) weiß, wie es welches Modul einzubinden hat, brauchen wir eine metadata.php-Datei. Diese legen wir im Modulverzeichnis /modules/eins_order_stats/ an.

<?php
/**
 * Metadata version
 */
$sMetadataVersion = '1.0';

/**
 * Module information
 */
$aModule = array(
    'id' => 'eins_order_stats',
    'title' => 'einscommerce Bestellstatistik',
    'description' => 'Eine grafische Auswertung der Bestellungen.',
    'version' => '1.0',
    'author' => 'einscommerce',
    'extend' => array(),
    'files' => array(
        'eins_order_stats' => 'eins_order_stats/controllers/eins_order_stats.php'
    ),
    'templates' => array(
        'eins_order_stats.tpl' => 'eins_order_stats/views/admin/tpl/eins_order_stats.tpl',
        'eins_order_stat_chart.tpl' => 'eins_order_stats/views/admin/tpl/eins_order_stat_chart.tpl'
    )
);

Das Modul bekommt damit eine eindeutige id, einen Namen unter dem es angezeigt werden soll, eine Beschreibung, ein Icon (wenn man möchte), eine Versionsnummer, einen Autor, ein Array, das beschreibt welche Klassen erweitert werden sollen, ein Array, das neu zu inkludierende Klassen enthält und ein Array, das die Pfade unserer Template-Dateien enthält. Da wir keine Klasse erweitern, sondern nur eine hinzufügen (unsere Controller-Klasse), bleibt das extend-Array leer und das files-Array bekommt unsere eins_order_stats.php. Im Array “templates” geben wir unsere beiden verwendeten Templates an.

Nun muss das Modul noch aktiviert werden. Dazu gehen wir im Admin-Menü auf Erweiterungen > Module (englisches Menü: Extensions > Modules). Wir wählen unser Modul aus und klicken im Tab “Stamm” auf aktivieren. Ein grüner Kasten zeigt uns jetzt an, dass unser Modul endlich startklar ist.

Oder klappt es immer noch nicht?

Debugging

Wenn es immer noch nicht klappt, sollte man per Ausschlussverfahren rausfinden woran es liegt. Meistens ist es ein kleiner Schreibfehler, ein Caching-Problem oder ein Fehler in der Controller-Klasse oder der Template-Datei.

Statt des Templates wird die Startseite des Shops geöffnet:

Dieser Fehler tritt häufig auf, wenn die View-Klasse oder die Template-Datei nicht gefunden wird, oder ein Smarty-Fehler aufgetreten ist. OXID zeigt dann als Fallback die Startseite an. Um herauszufinden welche der Fehlerquellen in Frage kommt, würde ich als erstes mal ein die(‘Controller-Klasse läuft!’) am Anfang der render()-Methode der View-Klasse einfügen. Wird die Controller-Klasse gefunden, wird einem das jetzt mitgeteilt. Dann kann man mal den Inhalt der Template-Datei durch ein Template läuft ersetzen. Wenn das funktioniert muss es sich wohl um einen Smarty-Fehler handeln. Wird schon die Controller-Klasse nicht geladen hat man möglicherweise in der metadata.php einen Tippfehler gemacht. Auch die metadata.php wird gecached, nach den Änderungen sollte man also noch einmal den tmp-Order leeren, um sicherzugehen, dass die Änderungen auch wirksam werden.

Zusätzlich kann man in der Datei config.inc.php den Debug-Modus aktivieren. Dann bekommt man zusätzliche Informationen am Ende der geladenen Seiten und sieht besser, ob es zu einem Fehler gekommen ist.

In der Datei /log/EXCEPTION_LOG.txt bekommt man weitere Informationen zu aufgetretenen Fehlern.



0 Kommentare

Dein Kommentar

An Diskussion beteiligen?
Hinterlasse uns Deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.