TEE-13 Frontend Meldeliste

Aus Contao Community Documentation

MsgError.png Unvollständiger Artikel: dieser Artikel ist noch nicht sauber bearbeitet.

Bitte erweitere ihn und entferne erst anschliessend diesen Hinweis.

Tagebuch einer Extension-Entwicklung

betrifft
TYPOlight Version ab TL 2.8
Extensions Extension Creator


Frontendmodul Meldeliste

So, weiter geht es mit dem vorerst letzten Schritt: Das Frontendmodul für die Meldeliste. Besondere Herausforderungen sind hier die Möglichkeit der Verknüpfung einzelner Einträge mit dem Frontendmodul für die Turnierpaardetails, und eine konfigurierbare seitenweise Anzeige der Einträge der Meldeliste (Da diese im Lauf der Zeit sehr umfangreich werden kann).

Die Klasse des Frontendmoduls heißt gwMeldeliste, und da ich sie schon bei der Erstellung im Extension-Creator angegeben hatte, ist diese schon als Frontendmodul registriert. Trotzdem checken wir das nochmal in /system/modules/gw_turnierpaare/config.php:

// Front end module 
array_insert($GLOBALS['FE_MOD']['turnierpaare'], 0, array 
( 
    'gw_turnierpaarliste' => 'gwTurnierpaarliste', 
    'gw_meldeliste'       => 'gwMeldeliste' 
));

Für die Konfiguration des Frontend-Moduls für die Darstellung benötige ich zwei Parameter: Die Anzahl der Datensätze pro Seite und die URL, auf die weitergeleitet werden soll, wenn man auf einen Tanzpaarnamen klickt (Das sollte eine URL sein, auf der das Frontendmodul der Turnierpaarliste eingebunden ist). Darum muss /system/modules/gw_turnierpaare/config/database.sql erweitert werden:

--
-- Extend table 'tl_module'
--
CREATE TABLE `tl_module` (
  `gw_tp_showonlyactive` char(1) NOT NULL default 'B',
  `gw_tp_couplesorting` char(1) NOT NULL default 'A',
  `gw_ml_pagesize` int(4) NOT NULL default '50',
  `gw_ml_coupledetails` varchar(255) NULL default NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

gw_ml_pagesize ist also ein int mit maximal 4 Stellen, gw_ml_coupledetails ein string.

Damit die beim Anlegen des Moduls auch angezeigt und editiert werden können, müssen wir an das DCA von tl_module ran, also in /system/modules/gw_turnierpaare/dca/tl_module.php einfügen:

$GLOBALS['TL_DCA']['tl_module']['palettes']['gw_meldeliste'] = '{title_legend},name,headline,type;{size_legend},gw_ml_pagesize, gw_ml_coupledetails;
{protected_legend:hide},protected;{expert_legend:hide},guests,cssID,space'; 
$GLOBALS['TL_DCA']['tl_module']['fields']['gw_ml_pagesize'] = array 
( 
    'label'                   => &$GLOBALS['TL_LANG']['tl_module']['gw_ml_pagesize'], 
    'default'                 => '50', 
    'exclude'                 => true, 
    'inputType'               => 'text', 
    'eval'                    => array('mandatory'=>true, 'minlength' => 1, 'maxlength'=>4, 'rgxp' => 'digit', 'tl_class' => 'w50') 
); 
$GLOBALS['TL_DCA']['tl_module']['fields']['gw_ml_coupledetails'] = array 
( 
    'label'                   => &$GLOBALS['TL_LANG']['tl_module']['gw_ml_coupledetails'], 
    'exclude'                 => true, 
    'inputType'               => 'text', 
    'eval'                    => array('mandatory'=>false, 'tl_class' => 'w50') 
);

Zunächst legen wir die neue Palette an, in der neben einigen Standardfeldern unsere beiden neuen Datenbankfelder stehen, und anschließend definieren wir diese Felder im DCA. Für die pagesize nehme ich einen Default von 50, ansonsten sind die Optionen inzwischen wohl bekannt.

Nun brauchen wir noch hübsche Labels für die neuen Felder im Backend.

/system/modules/gw_turnierpaare/languages/de/modules.php:

$GLOBALS['TL_LANG']['tl_module']['size_legend']     = 'Seitenlänge und Detail-URL'; 
$GLOBALS['TL_LANG']['tl_module']['gw_ml_pagesize'] = array('Meldungen pro Seite', 'Bitte geben Sie an, wieviele Meldungen pro Seite ausgegeben werden sollen'); 
$GLOBALS['TL_LANG']['tl_module']['gw_ml_coupledetails'] = array('URL der Paar-Detailseite' , 'URL, auf der die Paardetails ausgegeben werden, z.B. /turnierpaarliste/info/');

Entsprechend die englische Variante natürlich genauso.

Damit sind wir im Backend hier angekommen:

Backend Modulerstellung

Nun das Frontendmodul /system/modules/gw_turnierpaare/gwMeldeliste.php :

<?php if (!defined('TL_ROOT')) die('You can not access this file directly!'); 
/** 
 * Class gwMeldeliste  
 * 
 * @copyright  (C) 2010  
 * @author     Stefan Pfeiffer  
 * @package    Controller 
 */ 
class gwMeldeliste extends Module 
{ 
    /** 
     * Template 
     * @var string 
     */ 
    protected $strTemplate = 'gw_meldeliste'; 
    protected $strErrorTemplate = 'gw_meldeliste_error';

Zwei Templatenamen: Einer für die Ausgabe der Liste, der andere wenn etwas schief gegangen ist...

  public static $strPageKey = 'page';

Der URL-Bestandteil, der beim Blättern in der Liste unsere aktuelle Seite mit zählt. Mit 'page' also z.B. .../meldeliste/page/3.html um die dritte Seite anzeigen zu lassen.

    /** 
     * Generate module 
     */ 
    protected function compile() 
    { 
        $moduleParams = $this->Database->prepare("SELECT gw_ml_pagesize, gw_ml_coupledetails FROM tl_module WHERE id=?") 
                                       ->limit(1) 
                                       ->execute($this->id); 
        $pagesize = $moduleParams->gw_ml_pagesize; 
        $this->Template->coupledetails = $moduleParams->gw_ml_coupledetails;

Zunächst holen wir uns unsere beiden Modulparameter aus der Datenbank. die Detail-URL schreiben wir gleich ins Template, die pagesize merken wir uns erst mal.

    if ( strlen($this->Input->get(gwMeldeliste::$strPageKey)) ) 
    { 
        if(is_numeric($this->Input->get(gwMeldeliste::$strPageKey))) 
        { 
            $page = $this->Input->get(gwMeldeliste::$strPageKey); 
        } 
        else 
        { 
            // Error 
            $this->Template = new FrontendTemplate($this->strErrorTemplate); 
            return; 
        } 
    } 
    else 
    { 
        $page = 0; 
    } 
    $this->Template->page = $page;

Falls eine Seitenzahl in der URL angegeben, wird diese in $page gespeichert. Falls die Angabe nicht-numerisch ist, wird das Fehlertemplate ausgegeben. Und wenn keine Seitenzahl angegeben wird, tragen wir 0 ins Template ein.

    $limit = " LIMIT ".($pagesize*$page).",".$pagesize; 
 
    $testNextPage = $this->Database->execute("SELECT * from tl_gw_meldungen ORDER BY datum DESC LIMIT ".($pagesize*($page+1)).",1"); 
    if($testNextPage->numRows < 1) 
    { 
        $this->Template->nextpage = -1; 
    } 
    else 
    { 
        $this->Template->nextpage = $page+1; 
    }


Hier basteln wir uns das passende "LIMIT"-Statement für die aktuell ausgewählte Seite zusammen. Außerdem machen wir einen Testselect auf die erste row der darauf folgenden Seite. Falls diese existiert, wird die Nummer der nächsten Seite ins Template geschrieben, ansonsten eine -1 als Hinweis fürs Template.

Ich weiß, dass man diesen Check, ob eine weitere Seite existiert auch anders lösen kann, wahrscheinlich sogar eleganter. Aber erst mal funktioniert es :-).

    $arrMeldungen = array();

Hier kommen die ganzen Datensätze des Ergebnis rein...

    $objMeldungen = $this->Database->execute("SELECT * FROM tl_gw_meldungen ORDER BY datum DESC".$limit); 
 
    if($objMeldungen->numRows == 0) 
    { 
        // Error 
        $this->Template = new FrontendTemplate($this->strErrorTemplate); 
        return; 
    }

Ergebnisdatensätze aus der DB holen - wenn keine gefunden wurden, dann Error-Template ausgeben.

    while($newArr = $objMeldungen->fetchAssoc()) 
    {

Jede Row in ein assoziatives Array umwandeln...

      $objPaar = $this->Database->execute("SELECT * FROM tl_gw_turnierpaare WHERE id=".$newArr['pid']); 
 
      $name =   $objPaar->partnernachname; 
      if($objPaar->partnervorname) 
      { 
          $name .= ', '.$objPaar->partnervorname; 
      } 
      if($objPaar->partnerinnachname) 
      { 
          $name .= ' und '.$objPaar->partnerinnachname; 
      } 
      if($objPaar->partnerinvorname) 
      { 
          $name .= ', '.$objPaar->partnerinvorname; 
      } 
      $newArr['name'] = $name; 
      $newArr['paaralias'] = $objPaar->alias;

Und für jede Row aus der pid den Namen und den Alias des Turnierpaares bestimmen und mit ins assoziative Array aufnehmen.

      $arrMeldungen[] = $newArr; 
    } 
 
    $this->Template->meldungen = $arrMeldungen; 
    } 
} 
?>

Schließlich wird jeder Datensatz an das große Ergebnisarray angehängt und dann ins Template geschrieben.

Womit wir zu den Templates kommen. Zunächst /system/modules/gw_turnierpaare/templates/gw_meldeliste_error.tpl :

<div class="<?php echo $this->class; ?> block meldeliste"<?php echo $this->cssID; ?> 
<?php if ($this->style): ?> style="<?php echo $this->style; ?>"<?php endif; ?>> 
<h3>Es trat ein Fehler auf!</h3>

Erstmal easy as this, hier könnte man sich natürlich beliebig weiter austoben als nur mit diesem lapidaren Satz.

Spannender ist /system/modules/gw_turnierpaare/templates/gw_meldeliste.tpl:

<div class="<?php echo $this->class; ?>"<?php echo $this->cssID; ?><?php if 
($this->style): ?> style="<?php echo $this->style; ?>"<?php endif; ?>> 
<?php if ($this->headline): ?> 
<<?php echo $this->hl; ?>><?php echo $this->headline; ?></<?php echo $this->hl; ?>> 
<?php endif; ?>

Standardauftakt...

<table cellpadding="4" cellspacing="0" summary="Meldeliste"> 
  <thead> 
    <tr> 
      <th class="centered">Datum</th>
      <th class="centered">Ort</th> 
      <th class="centered">Turnier</th> 
      <th class="centered">Turnierart</th> 
      <th class="centered">Platz</th> 
      <th class="centered">Paare</th> 
      <th class="centered">Bemerkung</th> 
    </tr> 
  </thead>

Header der Tabelle...

  <tbody> 
<?php foreach ($this->meldungen as $meldung): ?>

Wir gehen durch alle rows im Array durch.

  <tr> 
  <td class="centered"> 
  <?php echo date('d.m.Y', $meldung['datum']); ?> 
  </td>

Datum "richtig" formatieren...

  <td><a href="<?php echo $this->coupledetails; ?><?php echo $meldung['paaralias']; ?>.html"><?php echo $meldung['name']; ?></a> 
  </td>

Hier wird der Paarname und als hinterlegter Link die Detail-URL mit dem Paar-Alias ausgegeben.

  <td class="centered"><strong><?php echo $meldung['turnierort']; ?></strong> 
  </td> 
  <td class="centered<?php if ($meldung['lat_std'] == 'Std'){echo ' std';} else { 
if ($meldung['lat_std'] == 'Lat'){echo ' lat';}}?>"><?php echo $meldung['startgruppe']; ?> <?php echo $meldung['startklasse']; ?> <?php echo $meldung['lat_std']; ?> 
  </td>

In Abhängigkeit von der Tanzart wird dem TD eine CSS-Klasse zugewiesen (zur farblichen Absetzung).

  <td class="centered"><?php echo $meldung['turnierart']; ?> 
  </td> 
  <td class="centered"><?php echo $meldung['platz_von']; ?> 
  <?php if (strlen($meldung['platz_bis']) > 0): ?> 
  <?php if ($meldung['platz_von'] != $meldung['platz_bis']): ?> 
  <?php echo " - ".$meldung['platz_bis']; ?> 
  <?php endif; ?> 
  <?php endif; ?> 
  </td> 
  <td class="centered"><?php echo $meldung['anzahlpaare']; ?> 
  </td> 
  <td><?php echo $meldung['bemerkung']; ?> 
  </td> 
  </tr><?php endforeach; ?> 
  </tbody> 
</table>

So weit unsere Tabelle...Es fehlen noch die Links, um eine Seite vor oder zurück zu springen...wenn es dann davor oder dahinter noch Seiten gibt.

<?php if ($this->page > 1): ?> 
<a id="prev" href="/{{env::page_alias}}/<?php echo gwMeldeliste::$strPageKey; ?>/<?php echo ($this->page-1); ?>.html"<<<</a> 
<?php elseif ($this->page == 1): ?> 
<a href="/{{env::page_alias}}.html"<<<</a> 
<?php endif; ?> 
&nbsp;Seite <?php echo ($this->page+1); ?>&nbsp; 
<?php if ($this->nextpage >= 0): ?> 
<a id="next" href="/{{env::page_alias}}/<?php echo gwMeldeliste::$strPageKey; ?>/<?php echo $this->nextpage; ?>.html">>>></a> 
<?php endif; ?> 
</div>

Das liefert uns im Frontend (ohne weitere CSS-Modifikationen) bei 6 Meldungen (nur zur Demo) pro Seite:

Meldeliste Seite 1

und beim Weiterschalten auf die zweite Seite (.../page/1.html):

Meldeliste Seite 2

Ein Klick auf den Turnierpaarnamen springt zur Detail-URL, bei mir /turnierpaarliste/info/<alias>.html:

Detailansicht

So, das soll es erst mal gewesen sein. Ich habe eine Menge gelernt, und würde den Code JETZT ganz anders aufziehen. Und das werde ich auch tun (haha). Allerdings ist der Zeithorizont unklar, und ich werde es auch nicht mehr schaffen "nebenbei" das Tagebuch zu führen. Pro Folge war es doch ca. eine Stunde Arbeit - mehr als gedacht! Mir fehlen für meinen Einsatz des Moduls auch noch einige kleine Features, aber das ist wirklich sehr speziell.

Zunächst werde ich gemeinsam genutzte Funktionen der Back- und Frontendmodule wohl in eine gemeinsame Klasse packen, um die Module und auch die Templates etwas zu entschlacken.

Vielleicht werde ich zu gegebener Zeit zum Thema "AJAX" in Frontendmodulen nochmal etwas schreiben, aber dann nur speziell auf diesen Teilbereich bezogen.

Es tut mir Leid, dass es hier die letzten Wochen etwas zäh und "langweilig" wurde, aber unerwarteter Weise war meine Freizeit (in der ich das hier tue) knapper als gedacht. Ich hoffe, Einige konnten teilweise Kleinigkeiten aus meinem Lernprozess mitnehmen , egal wie chaotisch er war, und für sich sinnvoll nutzen.

Diskussion aus dem Forum

user: deerwood

Zum Schritt 13 habe ich noch 2 Anmerkungen:

1. Statt Deinem Test für eine nächste Seite würde ich ein "SELECT COUNT(*) AS count FROM tl_gw_meldungen" machen, das kann MySQL sehr schnell, ohne wirklich auf die Datensätze zugreifen zu müssen. Basierend auf der Anzahl aller Datensätze und unter Berücksichtigung der Sätze pro Seite kann man dann eine beliebige Paginierung erzeugen (etwa auch "Erste/Letzte Seite" oder "5 Seiten weiter/zurück").

2. Statt die Meldungen erst zu selektieren und dann in der WHILE Schleife die Paar-Infos (für die Namen) einzeln nachzuselektieren solltest Du vor der Schelife einen JOIN zwischen 'tl_gw_meldungen' und 'tl_gw_turnierpaare' machen und in der Schleife dann nur noch die Namen zusammensetzen. Das ist erheblich performanter, es wird nur 1 statt 51 Statements abgesetzt und JOIN ist ja nun gerade eine der Stärken von relationalen Datenbanken.

Jedenfalls wünsche ich Dir sehr viel Erfolg bei Deiner geplanten Überarbeitung und ich würde mich SEHR freuen, wenn Du auch noch etwas zu AJAX schreibst.

user: dl1ely

zu deerwood 1. Ja, das geht sicherlich auch, und mit der Gesamtzahl der Datensätze kann man die von dir beschriebenen Dinge umsetzen. Ob das bezüglich der Performance wirklich so viel Unterschied macht? Ich glaube nicht, dass das wirklich messbar ist, wenn es einmal Seitenabruf aufgerufen wird. Ich überlasse das mal als Übung dem interessierten Leser ;-).

zu deerwood 2. Ja, da hast Du natürlich völlig recht. Ein Join ist deutlich besser. So wie ich das gemacht habe, war das quick'n'dirty, und sollte nicht als Vorbild dienen. Mir war erstmal unklar, welche Keys im assoziativen Array genutzt werden, wenn ich zwei Tabellen joine. Muss ich ein Feld wie "tabelle2.partnernachname" dann immer mit "AS" umbenennen? Ich muss mir mal anschauen, wie das in anderen Modulen gemacht wird, und dann schiebe ich nochmal eine Version nach, die es besser macht, und die Anzahl der Queries in der Tat drastisch reduziert :-).

JOIN-Power

Wie oben besprochen habe ich die Schleife über die Meldungen durch einen Join ersetzt. Im assoziativen Array werden einfach die Feldnamen aus jeder Tabelle benutzt, bei Konflikten gewinnt vermutlich die letztgenannte Tabelle(?).

/system/modules/gw_turnierpaare/gwMeldeliste.php sieht jetzt im unteren Teil so aus:

    $objMeldungen = $this->Database->execute("SELECT * FROM tl_gw_meldungen m, tl_gw_turnierpaare p WHERE m.pid = p.id ORDER BY datum DESC".$limit); 
 
    if($objMeldungen->numRows == 0) 
    { 
        // Error 
        $this->Template = new FrontendTemplate($this->strErrorTemplate); 
        return;
    } 
 
    $arrMeldungen = $objMeldungen->fetchAllAssoc(); 
    $templateResult = array(); 
    foreach($arrMeldungen as $meldung) 
    { 
        $name = $meldung['partnernachname']; 
        if($meldung['partnervorname']) 
        { 
            $name .= ', '.$meldung['partnervorname']; 
        } 
        if($meldung['partnerinnachname']) 
        { 
            $name .= ' und '.$meldung['partnerinnachname']; 
        } 
        if($meldung['partnerinvorname']) 
        { 
            $name .= ', '.$meldung['partnerinvorname']; 
        } 
        $meldung['name'] = $name; 
        $templateResult[] = $meldung; 
    } 
    $this->Template->meldungen = $templateResult; 
    } 
}

Also statt einer Schleife über alle Rows mit neuem SELECT für den Paarnamen nurnoch eine Schleife über alle Result-Rows, um aus den Namensbestandteilen den Anzeigenamen zu basteln...Wesentlich performanter!

Diskussion aus dem Forum

user: MBM

Detailseiten ich habe mir Dein Modul mal runtergeladen und es mal installiert und getestet. Es funktioniert auch soweit bis auf die Ansicht der Detailseite. Bekomme immer ein Page not found...

Ich frage mich wie Du das gelöst hast. Hat es vielleicht mit meiner Konfiguration des Apache Webserver zu tun? Ich benutze in der Apache Konfiguration mod_rewrite und das Contao CMS 2.8.3 liegt in einem ALIAS Ordner. Auch ist inder CMS Konfiguration die index.php Anzeige ausgeschaltet (eben mod_rewrite). URL: http://webserver/contao/

<alias "/<pfad>/contao_tuts">
RewriteBase /contao

user: dl1ely

Da wird bestimmt der Hund im Thema "Wie baue ich die Detail-URL zusammen" begraben sein. Wie sieht denn die "normale" URL der Meldeliste aus, und welche URL wird versucht aufzurufen, wenn Du den "Detail"-Link anklickst? Kannst den Domainnamen ja gerne weglassen.

Es kann gut sein, dass mein Ansatz für die Detail-URL nur bei bestimmten Randbedingungen funktioniert, dann müsste ich das nochmal nachbessern.

user: MBM Hooks

Zitat von dl1ely   

Hallo! Da wird bestimmt der Hund im Thema "Wie baue ich die Detail-URL zusammen" begraben sein. Hunde begraben  ? Mich würde ja interessieren unter welcher Konfiguration das bei Dir läuft. Nun ja, ich habe jetzt auch eine Lösung entwickelt. Ich weiß zwar auch nicht ob das Best Practise ist und ob es nicht einen einfacheren Weg gibt, aber es funktioniert.

Zitat von dl1ely   

Wie sieht denn die "normale" URL der Meldeliste aus, und welche URL wird versucht aufzurufen, wenn Du den "Detail"-Link anklickst? Kannst den Domainnamen ja gerne weglassen. Meine Konfigurationsparameter Im Backend ist URL's umschreiben auf aktiv gestellt.

Hauptlink: http://cms/contao/turnierpaareliste.html Detaillink: http://cms/contao/turnierpaareliste/info/test_testin.htm

  1. htaccess
RewriteBase /contao
RewriteRule ^(.*)/info/(.*)$ $1.html?info=$2
RewriteRule .*\.html$ index.php [L]
Zitat von dl1ely   

Es kann gut sein, dass mein Ansatz für die Detail-URL nur bei bestimmten Randbedingungen funktioniert, dann müsste ich das nochmal nachbessern.

Ich habe nun ein wenig experimentiert und zwei Lösungen gefunden. Die erste war Quick & Dirty und ist nicht update sicher und die zweite könnte gehen.

Lösung 1: index.php ergänzen (Nicht zu empfehlen)

    public function run() 
    { 
        global $objPage; 
        // Get page ID 
        $pageId = $this->getPageIdFromUrl(); 
        // filter  alias 
        if (stripos($pageId, '/info') >0 ) 
        { 
            $pageId = substr($pageId,0,stripos($pageId, '/info')); 
        } 
    }

Lösung 2: Hook

2.1 Ergänzung des TL-Hook array.

config.php - /system/modules/gw_turnierpaare/config/

$GLOBALS['TL_HOOKS']['getPageIdFromUrl'][] = array('gwHook', 'gwGetPageIdFromUrl');

2.2 Hook Klassendatei erstellen.

gwHook.php - /system/modules/gw_turnierpaare/

class gwHook extends Controller 
{ 
    public function  gwGetPageIdFromUrl($arrFragments) 
    { 
        // get alias (-url)     
        $url = explode('/', $arrFragments[0]) ; 
        // filter or cut, seo parameter and set only alias 
        $arrFragments[0] = $url[0] ; 
        return array_unique($arrFragments);  
    }       
}

user: dl1ely

Hallo MBM, sorry für die verspätete Antwort, aber ich war leider ziemlich beschäftigt. Ich kann dir auf Anhieb nicht sagen, warum das nicht so funktioniert wie bei mir.

In meiner .htaccess ist "RewriteBase /", da die Contao-Installation nicht in einem Unterverzeichnis liegt. Sonst habe ich auch "nur"

RewriteRule .*\.html$ index.php [L]

in der .htaccess .

Bei der Detail-URL schreibst du nur was von ".htm" am Ende der URL. Das ist aber sicherlich auch ".html", genau wie bei der Übersichts-URL, oder?

Bis auf die Rewritebase sehe ich erstmal keinen zwingenden Unterschied. Aber es wundert mich, dass es daran haken soll.

Dass du schon eine Menge gelernt hast zeigt ja dein Lösungsansatz mit dem Hook, das zeugt vom Lernerfolg ;-).

Ansichten
Meine Werkzeuge

Contao Community Documentation

Fork die Wand an!

Tristan Lins
Navigation
Verstehen
Verwenden
Entwickeln
Verschiedenes
Werkzeuge