Archiv der Kategorie: HTML

Ein kleiner Chat in PHP – wieder mal was für Minimalisten

Da ich mich derzeit viel im Chat in meinem Lieblings-Handarbeitsforum herumtreibe, dachte ich mir: so einen kleinen, eher spartanischen Chatroom solltest du eigentlich auch selber programmieren können, das kann so schwierig nicht sein. Bei Google gibt es -zig Beispiele, wie man so etwas angehen könnte, ich hab mich auf die Suche nach etwas Einfachem gemacht und bin hier bei php-einfach.de fündig geworden.

Ich hab allerdings ein paar Fehler bereinigt, auf PHP 7 umgeschrieben, die Ajax-Funktionalität eliminiert (warum kompliziert wenns auch einfach geht?), die MySQL-Tabelle komplett anders strukturiert, eine Teilnehmerliste hinzugefügt und sämtliche Formatierungen umgekrempelt, so dass letztendlich eine komplett umgeschmissene Logik dabei herausgekommen ist. Wie gesagt, die Grundidee stammt von php-einfach.de, und ich bedanke mich auch artig für den Denkanstoß. Aber ich denke, es ist genug kreative Eigenleistung dazugekommen, dass ich den Source hier als auf meinem Mist gewachsen einstellen kann.

Die Voraussetzungen

Viel ist nicht dran: man soll seinen Nick und seine Nachricht eingeben können, auf den Senden!-Button klicken, und dann soll im Chatfenster die Nachricht für alle sichtbar am unteren Ende des Fensters auftauchen. Ich hab noch die Uhrzeit wann die Nachricht geschrieben wurde mit dazugenommen, und protokolliere im Hintergrund auch das Datum mit, da ich in meinem Chat immer nur die Beiträge des heutigen Tages anzeigen möchte.

Aussehen tut das Ganze so:

screenshot_chat

screenshot_chat

Man kann seinen Nick eingeben (der bleibt auch nach dem Abschicken stehen), man kann seine Nachricht eingeben, und man kann auf den Senden!-Button klicken. Im Chatfenster scrollen die älteren Einträge nach oben weg, der neueste Eintrag landet immer ganz unten, und es wird noch die Uhrzeit zu jedem Beitrag angezeigt. Es werden nur die Einträge des heutigen Tages angezeigt, den Rest habe ich natürlich auch gebunkert, daraus könnte man noch ein Chat-Archiv erstellen. Rechts kriegt man noch eine Liste angezeigt, wer heute schon aller im Chat war, und das wars.

Im Hintergrund: natürlich eine MySQL-Tabelle

Die ist recht einfach strukturiert und heißt bei mir chat_daten:

chat_daten

chat_daten

Eine AutoIncrement-id als Primärschlüssel, und vier Textfelder für nick, eintrag, uhrzeit und datum, das war schon alles.

Der Sourcecode

Wir packen die Connect-Daten in eine externe Datei:

<?php 
$server    = 'localhost'; 
$user    = 'root'; 
$pass    = ''; 
$db    = 'chat'; 
$table    = 'chat_daten'; 
$conn=mysqli_connect($server, $user,$pass); 
mysqli_select_db($conn,$db); 
?>

Die Felder für Nick und Eintrag und den Senden-Button packen wir in ein Formular:

<?php  
//Config auslesen 
require_once('inc/config.php'); 
  
$heute = date("d.m.Y",time());
echo '<img src="el_logo.jpg">';
echo '<h1>Evis Chat live am '.$heute.'</h1>';

echo ' 
 <form action="index.php" method="post"> 
    <table border="0"> 
        <tr> ';
//Falls ein Nick eingegeben wurde, diesen als default in die Textbox setzen
if(!empty($_POST['nick'])){
    $akt_nick = $_POST['nick'];
}
else{
    $akt_nick='';
};
    
echo '<th>Nickname:</th><td><input type="text" name="nick" value="'.$akt_nick.'" id="textbox1"></td></tr>                 
        <tr> 
            <th>Nachricht:</th><td><input type="text" name="eintrag" value="" id="textbox">&nbsp;<input type="submit" name="eintragen" value="Senden!" id="button">&nbsp;</td> 
        </tr> 
    </table> ';

Damit man den Nick nicht immer neu eingeben muss, frage ich ab ob das Feld schon gesetzt wurde und schreibe dann den Wert als Default rein.

Dann starte ich, wenn auf den Button Senden geklickt wurde, und schreibe mir Nick, Eintrag, Datum und Uhrzeit in die Datenbank:

if(isset($_POST['eintragen'])){ 

if(empty($_POST['nick']) or empty($_POST['eintrag'])){ 
    echo '<script>alert("Bitte Nick UND Message eingeben")</script>'; 
}else{ 
    //Variablen definieren und mit "POST" Daten füllen (Mit htmlspecialchars filtern, Apostrophe maskieren..) 
    $nick = addslashes(htmlspecialchars($_POST['nick'])); 
    $eintrag = addslashes(htmlspecialchars($_POST['eintrag'])); 
    $uhrzeit = date("H:i",time());
    $datum = date("d.m.Y",time());

    
//Nick + Eintrag + datum+uhrzeit in die Datenbank schreiben 
    mysqli_query($conn,"INSERT INTO $table  
                      (nick,eintrag,uhrzeit, datum) VALUES 
                      ('$nick', '$eintrag', '$uhrzeit', '$datum')"); 
    } 

}

Falls kein Nick oder keine Nachricht eingegeben wurden, gibts einen Alert, andernfalls mache ich den Insert mit den aktuellen Daten.

Für die Ausgabe des Chats baue ich mir eine Div mit zwei darin enthaltenen Divs, weil ich Nick und Nachricht linksbündig, die Uhrzeit aber rechtsbündig ausgeben möchte. Das CSS dazu gibt es weiter unten, hier erstmal die Divs für das Chatfenster:

//alle Einträge von heute ausgeben
echo '<div id="chat_fenster" >';
        
        //Nur die Einträge von heute auslesen
          $abfrage = mysqli_query($conn,"SELECT * FROM $table WHERE datum LIKE '$datum'"); 
          while($row = mysqli_fetch_array($abfrage,MYSQLI_ASSOC)) 
          { 
            echo '<div id ="nick_und_message"><span style="color:red">'.$row['nick'].':</span><span style ="color:black">'.$row['eintrag'].'</span></div>
            <div id = "uhrzeit"><span style ="color:green">'.$row['uhrzeit'].'</span></div><br>'; 
          }
echo '</div>';

Als kleines Extra gebe ich rechts neben dem Chatfenster noch eine Liste aller Teilnehmer von heute aus:

//alle Teilnehmer von heute ausgeben
echo '<div id="teilnehmer_fenster" >';
        echo '<h2>Heute im Chat:</h2>';
        //Nur die Teilnehmer von heute auslesen
          $abfrage = mysqli_query($conn,"SELECT DISTINCT nick FROM $table WHERE datum LIKE '$datum'"); 
          while($row = mysqli_fetch_array($abfrage,MYSQLI_ASSOC)) 
          { 
            echo $row['nick'].'<br>'; 
          }
echo '</div>';

Jetzt noch ein wenig CSS-Kosmetik:

<style type="text/css"> 
    
    #textbox{ 
        width:600px; 
        border:1px solid blue; 
    } 
     
    #textbox1{ 
        width:80px; 
        border:1px solid blue; 
    }
    
    #button{ 

        border:1px solid #FF1493; 
        cursor:pointer; 
    } 
     
    #button:hover{ 
        border:1px solid #3399FF; 
    } 
    
    #nick_und_message{
        width:80%;
        float:left;
    }
    #uhrzeit {
        width:20%;
        float:right;
        text-align: right;
    }
    #chat_fenster{
        border:2px solid blue;
        width:750px; 
        height:350px; 
        padding:3px; 
        overflow: auto;
        float:left;
    }
    
    #teilnehmer_fenster{
        float:right;
        width:200px;
    }
</style>

Wichtig ist hier der Eintrag overflow:auto für das Chatfenster, der sorgt für den Scrollbalken. Damit jetzt die bereits vorhandenen Einträge nach oben wegscrollen und der letzte Eintrag immer im Chatfenster ganz unten auftaucht, noch ein kleiner Javascript-Trick:

<script>
window.onload = function()
{
//Chatfenster nach oben wegscrollen, damit immer die neuesten Einträge sichtbar sind
document.getElementById('chat_fenster').scrollTop = 9999999;
}
</script>

Man setzt einfach einen sehr hohen Wert für das Scrollen nach oben, damit ist die Liste immer nach oben weggescrollt und das Ende mit den neuesten Beiträgen bleibt im sichtbaren Bereich.

Das wars – viel Spaß beim Nachbauen! Das liesse sich natürlich noch beliebig erweitern, in einem CMS könnte man z.B. den Chat nur für eingeloggte User freischalten, dann entfällt das Abfragen des Nicks. Man könnte auch noch ein Chat-Archiv anlegen, in dem man die Beiträge der letzten Tage einsehen kann, und man könnte sonst noch allerhand Spielereien veranstalten, da kann sich jeder selber austoben. Ich lass es mal so stehen, als wirklich simple Lösung.

preg_replace & Co. im praktischen Einsatz

Ich bin immer noch an den exportierten Posts aus WordPress, die wir im Q&D-Editor schonmal manuell nachbearbeitet haben. Dabei sind sicher etliche Dinge aufgefallen, die man programmatisch korrigieren könnte, und wie man das praktisch angeht, schreib ich hier mal auf.

Wir haben immer noch die Ausgangstabelle rezepte, in der nur die ID, der post_content und der post_title drinstehen. Damit wir unsere Rohdaten nicht verhunzen, legen wir uns erstmal eine leere Kopie dieser Tabelle an, das geht im phpmyadmin unter „Operationen“, kopiere Tabelle, nur Struktur, oder mit dem simplen SQL:

CREATE TABLE rezepte_arbeit LIKE rezepte

Das kopiert die Struktur inklusive evtl. vorhandener Indizes etc. und wird unser Arbeitspferd. (Falls man mal die Daten mitkopieren möchte, CREATE TABLE table2 SELECT * FROM table1, aber das nur als Anmerkung am Rande)

Zur Kontrolle des Ergebnisses klemmt man sich die (noch leere) Arbeitstabelle in den Q&D-Editor. Spätestens jetzt macht es Sinn, den Namen der Tabelle auf eine Variable zu legen und die beiden SQLs entsprechend anzupassen.

//Arbeitstabelle festlegen
$arbeitstabelle = "arbeit_rezepte";
echo "<h1>Tabelle: ".$arbeitstabelle."</h1>";
...
$sql = "SELECT * FROM ".$arbeitstabelle." WHERE post_content LIKE '%".$filter."%'";
...
$sql = ('SELECT * FROM '.$arbeitstabelle.' WHERE post_content LIKE "%'.$filter.'%" 
LIMIT '.$limit.', '.$ergebnisse_pro_seite.'');

Wer es ganz genau haben möchte, legt auch noch den Dateinamen des Q&D-Editor Skripts auf eine Variable und paßt die Navigationslinks entsprechend an, aber das ist eigentlich schon Fleißarbeit:

//Dateinamen auf Variable legen
$datei = "arbeitspferd.php";
...
if($limit>0){echo '<a href="'.$datei.'?seite_nr='.($limit-1).'"><button type="button">Voriger Datensatz</button></a>';}
echo '<a href="'.$datei.'?seite_nr='.($limit+2).'"><button type="button">Nächster Datensatz</button></a><br>';
...
 //Nav Links alle Seiten
 for ($i=1; $i<=$row_total; $i++) { 
    echo "<a href='".$datei."?seite_nr=".$i."'>".$i."</a> "; 
};

Da die Tabelle noch leer ist, kriegt man erstmal gar nichts angezeigt:

arbeitstabelle_leer

arbeitstabelle_leer

Aber das ist ja OK so und wird sich gleich ändern.

Das Gerüst für die geplanten Aktionen

Ich bastle mir ein kleines Formular mit einem Button zum Starten des Skripts, und gebe der Übersicht halber mal alle Datensätze tabellarisch aus. Das sieht so aus:

<?php
//file:skript_starten.php
header('Content-Type: text/html; charset=utf-8');
require 'connection.php';

//Quell-und Zieltabelle festlegen
$quelltabelle = "rezepte";
$zieltabelle = "arbeit_rezepte";

echo "<h1>Skript starten</h1>";
echo "<h2> Quelle: ".$quelltabelle." Ziel: ".$zieltabelle."</h2>";

//Formular mit Button für Start
echo "<form action ='#' method = 'post'>";
echo "<input type = 'submit' name = 'absenden' value = 'Skript starten'>";
echo "</form>";

if(isset($_POST['absenden'])){
    
    echo "<script type=\"text/javascript\">alert('Skript gestartet');</script>";
    
    //Hier kommt die Action hin
    
    
    }

$sql = "SELECT * FROM ".$quelltabelle."";
$rs_result = $conn->query($sql);
$row_total = $rs_result->num_rows;

echo "Datensätze gesamt: ".$row_total."<br>";

 //Ausgabe in Tabelle - nur Arbeitsversion, kann man für den Betrieb rausnehmen
echo "<table border='1' cellpadding='4'>";
echo "<tr><th>ID</th><th>Titel</th><th>Content</th></tr>";
 
 while($row = $rs_result->fetch_assoc()) {
            
            echo "<tr><td valign='top'>".$row["ID"]."</td>";
            echo "<td valign='top'>".utf8_encode($row["post_title"])."</td>";
            //HTML Source in Textarea ausgeben
            echo "<td valign='top'><textarea rows='5' cols='80'>".utf8_encode($row["post_content"])."</textarea>";
            echo "</td></tr>";
            
 } //Ende von while row = rs_result
 
 echo "</table>";
 
?>

Ich hole mir mit dem require die Connection, lege Quell-Und Zieltabelle auf Variable und baue mir den Button für das Starten des Skripts. Die Kontrollausgabe der aktuellen Quelltabelle ist nicht unbedingt notwendig, das dient nur der Übersicht.

Jetzt gehts zur Sache

Der Ablauf ist folgender: die Datensätze aus der Quelltabelle werden selektiert und die Felder auf Variablen gelegt. An den Variablen nimmt man dann die nötigen Manipulationen vor, und die fertig bearbeiteten Datenfelder werden in ein Array geschrieben. Aus dem Array füllt man dann mit Insert die Zieltabelle. Das Ergebnis kann man dann gleich mal im Q&D-Editor überprüfen. Ich mach dann gleich noch ein Beispiel, hier zuerst mal der Source:

if(isset($_POST['absenden'])){
    
    echo "<script type=\"text/javascript\">alert('Skript gestartet');</script>";
    
    //Hier kommt die Action hin
    
    $sql = "SELECT * FROM ".$quelltabelle."";
    $rs_result = $conn->query($sql);
    
      
    $i=0;
    $temp = array();
    
    while($row = $rs_result->fetch_assoc()) {
            
            $akt_id =$row["ID"];
            $title = $row["post_title"];
            $content = $row["post_content"];
            
            // Hier kann man beliebige Stringfunktionen auf die Felder anwenden
            $content = strip_tags($content);
            
            //Behandelte Strings in Array einlesen
            $temp[$i][id] = $akt_id;
            $temp[$i][titel] = $title;
            $temp[$i][content]= $content;
            $i = $i+1;
            
    }
            
 }
$len_temp = count($temp);

//Array zeilenweise in Zieltabelle wegschreiben
for ($i = 0; $i < $len_temp; $i++){    
  
          $temp_id = $temp[$i][id];
          $temp_titel = $temp[$i][titel];
          $temp_content = $temp[$i][content];
          
          //addslashes ist nötig weil sonst der Insert bei jedem Hochkomma aussteigt
          $sql_insert = "INSERT INTO ".$zieltabelle." (ID, post_title, post_content) 
          VALUES (".$temp_id." ,'".addslashes($temp_titel)."' , '".addslashes($temp_content)."')";
          
          if (mysqli_query($conn, $sql_insert)) {
                      echo "Insert erfolgreich<br>";
                      
                    }
                      
                    else {
                      echo "Fehler beim Update: " . mysqli_error($conn);
                      
                    }
} // end for

Das wars schon – das Skript braucht ein paar Sekunden, nicht ungeduldig werden. Wichtig ist, dass man die einzufügenden Felder beim Insert noch mit einem addslashes() behandelt, weil sonst bei jedem Anführungszeichen im Text das Skript aussteigt.

Ich habe oben nur eine Ersetzung ausgeführt, nämlich das:

$content = strip_tags($content);

Das führt in der Praxis evtl. zu unerwünschten Effekten, weil es wirklich gnadenlos alle HTML-Tags raushaut. Für meine Zwecke besser geeignet ist der strip_tags mit Ausnahmen:

$content = strip_tags($content,'<h2>, <h3>, <ol>, <ul>, <li>');

Das läßt meine Überschriften und die Listen drin, sieht schon besser aus.

mit_liste

mit_liste

Aber da muss jeder selber tüfteln, was für seine Zwecke am besten geeignet ist. Fröhliches googlen nach praktikablen Stringbehandlungen! Und nicht vergessen vor erneutem Starten des Skripts die Zieltabelle mit Truncate zu leeren, sonst fällt der Insert wegen doppelter IDs auf die Nase.

Nachtrag: den Truncate kann man natürlich auch ins Skript mit einbauen, einfach die Action um folgenden Code ergänzen:

//Zieltabelle leeren
    $sql_zap="TRUNCATE TABLE ".$zieltabelle."";
    if (mysqli_query($conn, $sql_zap)) {
                      echo "Truncate erfolgreich<br>";
                      
                    }
                      
                    else {
                      echo "Fehler beim Truncate: " . mysqli_error($conn);
                      
                    }

Das muss natürlich vor dem Insert rein, am Besten setzt man es ganz an den Anfang innerhalb der if(isset($_POST…)-Bedingung. Dann spart man sich das manuelle Leeren der Zieltabelle.

Quick&Dirty Editor Sourcecode

Der Editor kommt mit erstaunlich wenig Code aus, das sind gerade mal knapp 90 Zeilen. Ich gehe das hier mal der Reihe nach durch. Meine Ausgangstabelle heißt rezepte und sieht so aus:

ausgangstabelle

ausgangstabelle

Sie enthält nur die rudimentären Daten meiner Rezepte aus dem Inselfisch-Kochbuch, und zwar die ID, den post_content und den post_title.

tabelle_rezepte

tabelle_rezepte

Wir ändern nur den post_content, Titel und ID bleiben unangetastet.

Zuerst bastle ich mir natürlich die Datenbankconnection:

<?php
//file:connection.php
error_reporting(0);
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "conversion";

 
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
} else {echo "Datenbank Connection OK<br>";}
?>

Die wird per require reingeholt, und dann kanns losgehen. Ich habe hier den Algorithmus aus der nummerischen Pagination zweckentfremdet und lasse mir pro Seite genau einen Datensatz anzeigen.

<?php
//file:seitenweise.php
header('Content-Type: text/html; charset=utf-8');
require 'connection.php';


$sql = "SELECT * FROM rezepte";
$rs_result = $conn->query($sql);
$row_total = $rs_result->num_rows;
$gesamte_anzahl = $row_total;

echo "Datensätze gesamt: ".$row_total."<br>";

/* 1 Datensatz pro Seite festlegen*/
$ergebnisse_pro_seite = 1;
$gesamt_seiten = ceil($gesamte_anzahl/$ergebnisse_pro_seite);

if (empty($_GET['seite_nr'])) {
    $seite = 1;
} else {
    $seite = $_GET['seite_nr'];
    if ($seite > $gesamt_seiten) {
        $seite = 1;
    }
}

$limit = ($seite*$ergebnisse_pro_seite)-$ergebnisse_pro_seite;
if($limit<0){$limit =0;}

Zuerst zähle ich die Datensätze, dann lege ich fest, dass pro Seite genau ein Datensatz angezeigt werden soll. Defaultmässig landet der Editor bei Seite 1. Dann berechne ich mir das Limit, also beim wievielten Datensatz die Ausgabe beginnen soll. Das verwenden wir gleich mal für die Navigationsbuttons „Voriger Datensatz“ und „Nächster Datensatz“:

//Debug-Ausgaben und Nav-Buttons
echo "Datensatz = ".($limit+1)." von ".$row_total."<br>";
echo "Datensätze pro Seite = ".$ergebnisse_pro_seite."<br>";
if($limit>0){echo '<a href="seitenweise.php?seite_nr='.($limit-1).'" style="font-weight: bold;"><button type="button">Voriger Datensatz</button></a>';}
echo '<a href="seitenweise.php?seite_nr='.($limit+2).'" style="font-weight: bold;"><button type="button">Nächster Datensatz</button></a><br>';

Dann hole ich mir auch schon den Datensatz mit der aktuellen Seitennummer:

//Selektiere Datensatz mit der aktuellen Seitennummer
$sql = ('SELECT * FROM rezepte LIMIT '.$limit.', '.$ergebnisse_pro_seite);
$result = $conn->query($sql);

Da meine Variable $ergebnisse_pro_seite den Wert 1 hat, wird ein Datensatz pro Seite ausgegeben. Den zeige ich mir in einer while-Schleife in einer zweispaltigen Tabelle an. Die Tabelle enthält eine textarea, in die kommt der HTML-Code. In der zweiten Spalte wird die Vorschau ausgegeben. Zuerst schubse ich mal die ID des aktuellen Datensatzes in eine Variable, die brauchen wir später für den Update.

Dann frage ich ab, ob der „Änderungen speichern“-Button gedrückt wurde. Wenn ja, zeige ich den geänderten Inhalt des Editorfensters in der Tabelle an, wenn nein einfach den Originalcontent so wie er aus der Abfrage kommt.

//Ausgabe in Tabelle mit Formular
 while($row = $result->fetch_assoc()) {
            $akt_id = $row["ID"];
            echo "Aktuelle Datensatzid: ".$akt_id."<br>";
            
            echo "<form action ='#' method = 'post'>";
            echo "<input type = 'submit' name = 'absenden' value = 'Änderungen speichern'>";
            echo "<table border='1' cellpadding='4'>";
            echo "<th>".utf8_encode($row['post_title'])." HTML</th><th>Vorschau</th></tr>";
            
            //Wenn Speichern gedrückt wurde, editierten Content aus der Textarea ausgeben 
            if(isset($_POST['absenden'])){$inhalt = $_POST['content'];}
            else {$inhalt = utf8_encode($row['post_content']);}
                        
            echo "<tr>";
            //HTML Source in Textarea ausgeben
            echo "<td valign='top'><textarea rows='50' cols='80' name='content' id='content'>".$inhalt."</textarea>";
            echo "</td>";
            
            //HTML Vorschau ausgeben
            echo "<td valign='top'>".$inhalt."</td>";
            echo "</tr>";
            echo "</table>";
            echo "</form>";

Jetzt fehlt nur noch der Update, der wird beim Klicken des „Änderungen speichern“-Buttons ausgelöst:

if (isset($_POST['absenden'])){
                            
            //***********Action
            
            $sql = "UPDATE rezepte SET post_content =
'".utf8_decode(addslashes($_POST['content']))."' WHERE ID=".$akt_id."";
            //*************End Action
            
           if (mysqli_query($conn, $sql)) {
              echo "Record updated successfully";
              echo "<script type=\"text/javascript\">alert('Änderungen erfolgreich gespeichert');</script>";
            }
              
            else {
              echo "Error updating record: " . mysqli_error($conn);
              echo "<script type=\"text/javascript\">alert('Fehler beim Speichern');</script>";
            }    
    
    } //Ende von isset absenden
 } //Ende von while row = result

Im Update überschreibe ich den Inhalt von post_content mit dem Inhalt des Editorfensters, die steckt ja in der Variablen:

$_POST['content']

und zwar im Datensatz mit der aktuellen ID. Wegen der deutschen Umlaute muss ich hier mit utf8- encode und -decode arbeiten, das muß man bei einer anderen Datenbankkollation evtl anpassen. Ausserdem brauche ich noch einen addslashes(), weil meine Texte Anführungszeichen enthalten können. Dann mache ich noch eine kleine Fehlerbehandlung und gebe einen Javascript-Alert aus, ob der Update funktioniert hat oder nicht. Das wars auch schon – jetzt fehlt nur noch die nummerische Pagination zum anklicken des i-ten Datensatzes:

 //Nav Links alle Seiten
 for ($i=1; $i<=$row_total; $i++) { 
    echo "<a href='seitenweise.php?seite_nr=".$i."'>".$i."</a> "; 
};

Bitteschön, fertig ist der Q&D-Editor!

vanillekipferl_im_editor

vanillekipferl_im_editor

Tipp, auch quick&dirty:

Man kann ganz schnell eine (primitive) Filterfunktion basteln, wenn man die beiden SQL-Strings um eine „LIKE“-Klausel ergänzt:

$sql = "SELECT * FROM rezepte WHERE post_content LIKE '%".$filter."%'";
...
//Selektiere Datensatz mit der aktuellen Seitennummer
$sql = ('SELECT * FROM rezepte WHERE post_content LIKE "%'.$filter.'%" LIMIT '.$limit.', '.$ergebnisse_pro_seite.'');

Die Variable $filter setzt man am Anfang des Skripts auf den gewünschten Wert, zum Beispiel auf „caption“ oder „<ul>“ oder was auch immer. Dabei muss man aber berücksichtigen, was die aktuelle Datenbankkollation als Suchkriterien akzeptiert, bei Umlauten und Sonderzeichen kommt man da sonst zu unerwarteten Ergebnissen. Aber so als kurze Arbeitshilfe ist das schon mal ganz nützlich.

Quick&Dirty Editor für HTML-Posts mit Vorschaufunktion

Wer viel mit CMS umgeht, kennt das zur Genüge: in der Datenbank landet im Feld für den Inhalt eines beliebigen Posts oder einer Seite Text mit jeder Menge HTML-Tags, schließlich arbeitet man ja mit dem TinyMCE oder einem seiner Verwandten.Das sieht zum Beispiel in WordPress so aus:

<h2>Einleitung</h2>
In München-Haidhausen gibt es einen wunderbaren Laden, den "Liquid", da kann man erlesene Alkoholika auch in kleinen Mengen (ab 0,1 l) kaufen. Daher gibt es bei mir die Scaloppine mal mit Marsala, mal mit Sherry, oder ich bereite sie mit Portwein zu, ganz nach Lust und Laune. Wichtig ist, daß der Wein eher von der lieblichen Sorte sein soll, nur dann bekommt die Sauce den schönen vollmundigen Geschmack.
<h2>Zutaten</h2>
Für 4 Personen:

4 Kalbsschnitzel oder 8 dünn geschnittene Minutensteaks, alle Fettränder und Häutchen sorgfältig abgeschnitten, Mehl zum wenden. Je 1 El Butter und gutes Olivenöl, Salz, Pfeffer.
Zum Ablöschen: 1 kleines Glas lieblicher Südwein.
Noch 1 El. eiskalte Butter.
<h2>Zubereitung</h2>
Siehe <a href="http://evileu.de/inselfisch-kochbuch/2017/01/12/schnitzel-mit-zitrone-scaloppine-al-limone/">Scaloppine Grundrezept</a>, zum Ablöschen ein kleines Glas Südwein (s.oben) nehmen. Man kann die Scaloppine auch noch mit etwas feingehackter Petersilie bestreuen, aber das muß nicht unbedingt sein.

Das ist der post_content eines Rezeptes für Scaloppine. Im Inselfisch-Kochbuch sieht das so aus:

scaloppine

scaloppine

Recht und schön, aber im richtigen Leben kommt es immer wieder mal vor, dass man Content aus einem CMS abzieht und in einem anderen CMS weiterverwenden möchte. Das geht in den seltensten Fällen ohne umfangreiche Nacharbeiten, und nicht alles kann man programmgesteuert erledigen.

Man kann mit PHP natürlich alle HTML-Tags entfernen (siehe strip_tags Doku) und dabei sogar Ausnahmen definieren, welche Tags drinbleiben dürfen, und man kann sich auch mit preg_replace mehr oder weniger geniale Ersetzungen einfallen lassen. Aber das hat auch so seine Nachteile.

In dem Scaloppine-HTML ist zum Beipiel ein Link drin, der auf das Inselfisch-Kochbuch auf evileu.de verweist, das kann natürlich nicht so bleiben. Schmeißt man alle Links programmatisch komplett raus, ergibt der Text keinen Sinn mehr, da fehlt dann für das ganze Rezept die Zubereitung. Hier müßte man, wenn man es ganz richtig machen will, dem Link nachgehen und den entsprechenden Zubereitungstext rauskopieren. Das ist natürlich aufwendig, aber im Zweifelsfall die einzig richtige Lösung.

Tscha, was macht man jetzt, wenn man vor ein paar Hundert Posts sitzt, die manuell überarbeitet werden müssen? Man überlegt sich eine Arbeitserleichterung.

Voraussetzungen

Ich hab mir mal aus der WordPress posts-Tabelle nur die minimalsten Rohdaten abgezogen, nämlich die ID, den Titel und den Content, gefiltert nach post_type = post und post_status=publish, das sind die Kerndaten aller veröffentlichten Kochrezepte. Wenn ich sonst noch was brauchen sollte, den Autor oder das Datum oder sogar die Kategorien oder sonstwas, das kann ich mir später über die ID wieder dazulinken. Das selbe könnte ich auch mit Joomla-Beiträgen oder Drupal-Nodes machen, Hauptsache die ID kommt mit und der Titel und Content passen. Und das Ganze ist eigentlich nur sinnvoll, wenn die Posts nicht allzu lang sind, aber meine Kochrezepte gehen selten über eine Bildschirmseite hinaus.

Wir basteln uns einen Quick&Dirty Editor

Damit man jetzt nicht immer direkt in der Datenbank rumpfuschen muss, wenn man in einem HTML-Post etwas ändern will, habe ich mir eine zwar nur als Arbeitspferd geeignete, aber durchaus zeitsparende Lösung überlegt. Ich habe bei meinen ganzen Snippets zur Pagination gekupfert und mir ein PHP-Script gebaut, in dem pro Seite ein Datensatz dargestellt wird. Es gibt eine rudimentäre Navigation (voriger/nächster Datensatz), das reicht mir für die Arbeit, schließlich gucke ich im Zweifelsfall einen Datensatz nach dem anderen durch. Dann habe ich mit Hilfe einer Tabelle zwei Bildschirmbereiche konstruiert, im ersten wird der HTML-Code dargestellt und kann editiert werden, im zweiten gibt es eine Vorschau des Ergebnisses. Noch ein Knöpfchen zum Speichern der Änderungen, und es kann losgehen.

qe_scaloppine

qe_scaloppine

Für den Fall dass ich zu einem bestimmten Datensatz springen möchte habe ich ganz unten auch noch eine nummerische Navigation eingebaut, das ist bei über 300 Datensätzen ganz nützlich.

seitenpagination

Seitenpagination

Wie man mit dem Editor arbeitet

Wenn man in einem Post etwas entdeckt hat, das man ändern möchte, kann man dies direkt im HTML-Fenster tun. Hier habe ich zum Beispiel ein Bild drin, das wird WordPress-typisch in einen caption-Shortcode eingeklemmt, der sorgt in WordPress für die korrekte Darstellung der Bildbeschriftung.

caption

caption

Ein anderes CMS kennt aber den Shortcode nicht, also muss er raus. Das sieht man auch an der Darstellung im Vorschau-Fenster, der Browser kann natürlich ohne WordPress mit dem Shortcode nix anfangen und stellt ihn als Text dar:

caption_vorschau

caption_vorschau

Um das zu korrigieren, schmeisse ich im HTML-Fenster die caption-Tags raus und platziere noch ein paar <br> an den geeigneten Stellen, mache gleichzeitig noch einige kleine Textkorrekturen:

ohne_caption

ohne_caption

Klick auf den Button „Änderungen speichern“, dann kommt erst mal eine kurze Meldung, ob der Update erfolgreich war:

meldung

meldung

Und dann kriegt man das Ergebnis im Vorschaufenster angezeigt:

vorschau

vorschau

Na bitte, das geht doch ratzfatz!

Ohne Netz und doppelten Boden

Da ich beim Klicken auf den „Änderungen speichern“-Button direkt einen Update auf die Datenbank fahre, sind die Änderungen natürlich nicht mehr rückgängig zu machen, aber das kann ich verschmerzen. Schlimmstenfalls muss ich mir einen Datensatz aus der Originaltabelle wieder herholen, wenn ich ihn total verhunzt habe, aber das ist mir eigentlich noch nicht passiert. Im richtigen Leben wird sowas eh der Praktikant machen, und der kann HTML und weiß was er tut, wenn er Tags rausschmeisst oder korrigiert.

Der Q&D-Editor ist ja auch nur als Arbeitshilfe für Entwickler gedacht, und nicht für den Endanwender. Er soll auch nicht den gezielten Einsatz von programmatischen preg_replace-Aktionen ersetzen, die haben ja durchaus auch einen Sinn. Mir gehts immer so, dass ich beim Arbeiten mit dem Editor meistens ganz schnell merke, welche Elemente programmgesteuert rausgeschmissen werden können, weil ich immer die selben Tags editiere – und dann ist natürlich ein bisschen zusätzliches PHP angesagt. In der Praxis wird man immer eine Kombination aus manuellem Editieren und programmgesteuerten Ersetzungen fahren, damit bin ich eigentlich bis jetzt recht gut klargekommen.

Und jetzt gibts den Sourcecode des Q&D-Editors, in einem neuen Beitrag.

 

Alphabetische Paginierung aus Array: die Tücken des PHP-sort()

Eigentlich hats mich ja nur interessiert, ob das recht aufwendig ist, aber das hielt sich in Grenzen, deswegen mach ich es noch der Vollständigkeit halber. Voraussetzung ist wie im letzten Beitrag, dass die auszugebenden Daten in Form eines Arrays vorliegen, ich nehme wieder meine 334 Kochrezepte als JSON, so wie im letzten Beitrag zur nummerischen Pagination. Nach dem json_decode sieht die Struktur des Arrays wie gehabt wie folgt aus:

array(1) 
{ ["rezepte"]=> array(334) {
 [0]=> array(3) { ["ID"]=> string(4) "1828" ["post_title"]=> string(31) "Pudding nach Art des Hauses Leu" ["post_content"]=> string(37) "Vanille oder Schoko oder Johannisbeer" }
 [1]=> array(3) { ["ID"]=> string(4) "1827" ["post_title"]=> string(5) "Milch" ["post_content"]=> string(23) "reicht nicht bis morgen" } 
 [2]=> array(3) { ["ID"]=> string(4) "1825" ["post_title"]=> string(14) "Petersilwurzel" ["post_content"]=> string(29) "hab ich abgeerntet, ist nicht" }

Jetzt interessieren mich aber eigentlich nur die post_title, die krieg ich mit:

$akt_titel = $dataObject['rezepte'][$i]['post_title'];

Und die Titel der Rezepte möchte ich nach Buchstaben seitenweise sortiert ausgeben. Also, pack’mas.

Das Formular mit den Buttons

Dafür fülle ich mir erst einmal ein Array mit den Buchstaben a-z, daraus baue ich dann das Formular:

//Buchstaben a-z in Array schreiben
$letters = array();
for ($i = 'a', $j = 1; $j <= 26; $i++, $j++) {
    $letters[$j] = $i;    
}
//Formular mit Buttons a-z erzeugen
echo "<form action = '#' method = 'post'>";

for ($i=1; $i <=26; $i++){
    echo "<input type='submit' id='el_button' name='".$letters[$i]."' value='".$letters[$i]."'>";
}
    echo "</form>";

Das sieht jetzt erstmal so aus:

alfa_buttons

alfa_buttons

Dann ermittle ich, welcher Buchstabe angeklickt wurde, und rufe mit diesem Buchstaben als Parameter meine Ausgabefunktion auf:

//Ermitteln welcher Buchstabe angeklickt wurde und Ausgabefunktion aufrufen
for ($j = 1; $j <= 26; $j++){    
        if (isset($_POST[''.$letters[$j].''])){
            
            return el_aufruf("".$letters[$j]."");
        }
        
    }

In der Ausgabefunktion hole ich mir mein Array mit allen Daten, steppe es ganz durch und picke mir mit dem if nur die Einträge heraus, wo der post_title mit dem als Parameter übergebenen Buchstaben anfängt. Diese werden mit array_push einer nach dem anderen an mein Hilfsarray $ausgabe angehängt.

function el_aufruf($stabe){
            
    //JSON alle Rezepte abholen und in Array umwandeln
    $json = file_get_contents('http://localhost/kleine_api/rezepte/rezeptelesen.php');
    $dataObject = json_decode($json,true);
    $anzahl = count($dataObject['rezepte']);
    echo "Rezepte insgesamt = ".$anzahl."<br>";
            
    //Hilfsarray für Ausgabe     
    $ausgabe = array();
        
        for ($i=0; $i<$anzahl; $i++){
            //Ersten Buchstaben vergleichen
            if (strtoupper(substr(($dataObject['rezepte'][$i]['post_title']), 0, 1))== strtoupper($stabe)){
                //Hilfsarray mit Titeln zum aktuellen Buchstaben füllen
                array_push($ausgabe, $dataObject['rezepte'][$i]['post_title']);
            }
        }

Das Hilfsarray sortiere ich mir noch mit dem einfachen sort() und gebe es zeilenweise aus:

//Array sortieren und ausgeben
    sort($ausgabe);
    foreach($ausgabe as $out){
        echo $out."<br>";
    }
} //end function el_aufruf

Die Ausgabe sieht dann beispielsweise so aus:

buchstabe_c

buchstabe_c

Kleiner Pferdefuß: die Umlauts und Sonderzeichen

Der PHP sort() ist nicht dazu zu bewegen, die deutschen Umlaute anderswo als am Ende einer Liste nach dem Buchstaben Z einzusortieren. Auch Sonderzeichen wie z.B. mit Accents (Béchamel) landen gnadenlos am Ende. Das sieht man ganz deutlich am Ende der Liste zum Buchstaben B:

buchstabe_b

buchstabe_b

Da müsste man jetzt hingehen und eine eigene sort-Funktion schreiben, die aus den deutschen Umlauts Diphtonge mach (ä=ae, ö=oe…) und erst danach sortieren, vor der Ausgabe aber die Umlaute wieder einsetzen. Ach nö Leute, das ist mir echt zu stressig. Ich lebe mal mit der Ausgabe der Umlaute am Ende, vielleicht begegnet mir ja beim Googlen noch irgendwo eine elegantere Lösung.

Und prompt hab ich was gefunden: Sortierung der Umlauts wie im Telefonbuch

Auf der Webseite von Marco Krings habe ich folgende interessante Sortierfunktion gefunden:

http://www.marcokrings.de/arrays-sortieren-mit-umlauten/

Er arbeitet mit zwei Arrays für die Umlaute und die entsprechenden Diphtonge. Diese Arrays könnte man jetzt noch um die Accent-Zeichen wie z.B. é ergänzen, ich hab das mal ausprobiert:

$aSearch   = array("Ä","ä","Ö","ö","Ü","ü","ß","-","é");
    $aReplace  = array("Ae","ae","Oe","oe","Ue","ue","ss"," ","e");

Funktioniert eins a!

Meine Ausgabe wird jetzt noch um Marcos Funktion ArraySort() erweitert:

//Array sortieren und ausgeben
    //sort($ausgabe);
    $umlauts = ArraySort($ausgabe);
    foreach($umlauts as $out){
        echo $out."<br>";
    }

Jetzt paßt die Sache. die Umlaute und das é werden richtig einsortiert:

e_accent

e_accent

Das ist ausbaufähig! Vielen Dank an Marco für die geniale Vorlage.

Nachtrag: woher nehm ich jetzt die ID?

Ich habe in meiner alfabetischen Ausgabe bis jetzt nur die Titel der Rezepte verarbeitet, das reicht aber unter Umständen nicht. Für manche Zwecke wäre es schick, wenn man auch noch die ID mitnehmen könnte, zum Beispiel in WordPress für die Ausgabe des Permalinks. Man könnte jetzt auf die Idee kommen, in der for-Schleife den array_push umzustricken, so dass ein mehrdimensionales Array mit Titel und ID aufgebaut wird… ja Pustekuchen, dann fällt unser schöner Sortieralgorithmus auf die Nase, das geht leider nicht. Ich hab mir da eine quick&dirty-Lösung einfallen lassen: ich hänge die ID einfach an den Titel mit dran, und setze dazwischen als Trennzeichen ein #. Kann ich so machen, weil in meinen Rezepttiteln das Zeichen # sonst garantiert nirgendwo vorkommt. Die for-Schleife sieht dann so aus:

for ($i=0; $i<$anzahl; $i++){
  //Ersten Buchstaben vergleichen
if (strtoupper(substr(($dataObject['rezepte'][$i]['post_title']), 0, 1))== strtoupper($stabe)){
//Hilfsarray mit Titeln und ID zum aktuellen Buchstaben füllen (Trennzeichen#)
array_push($ausgabe, $dataObject['rezepte'][$i]['post_title']."#".$dataObject['rezepte'][$i]['ID']);
   }
}

Dann sieht die Ausgabe erstmal so aus:

id_angehaengt

id_angehaengt

Das zerpflücke ich mir jetzt noch in die Teilstrings vor und nach dem #, gebe nur den Titel aus und kann dann mit der ID noch was anderes anfangen. Die Ausgabeschleife sieht dann so aus:

foreach($umlauts as $out){
        
        //alle Zeichen vor dem # = Titel
        $hilf_titel = substr($out, 0, strpos($out, '#', 0));
        echo $hilf_titel."<br>";
        
        //alle Zeichen nach dem # = ID
        $hilf_id = substr(strrchr($out, "#"), 1);
        //hier kann man mit der ID weiterarbeiten
        }

Wie gesagt, das ist ein wenig quick&dirty, aber es funktioniert, und ich kann den schicken Sortieralgorithmus von Marco einwandfrei weiterverwenden.

Nummerische Pagination mit Werten aus Array (pure PHP)

Das hat jetzt mit WordPress nur noch insofern etwas zu tun, als es einem auch hier mal unterkommen kann, dass die Daten für eine seitenweise nummerierte Ausgabe in Form eines Arrays ankommen und nicht direkt mit SQL aus der Datenbank abgefragt werden. Typischerweise passiert sowas, wenn man über eine API auf die Datenbank zugreift und die Ergebnisse der Abfrage als JSON zurückgegeben werden. Das kann man mit Hilfe von json_decode() flugs in ein Array umwandeln und mit einem foreach() wie gehabt seitenweise ausgeben. Wenn allerdings die Datensätze pro Seite einen Rest ergeben, fliegt PHP bei der letzten Seite auf die Nase und spuckt so etwas aus:

Notice: Undefined offset: 334 in C:\xampp\htdocs\kleine_api\err_num_page.php on line 74

Die kleine Tücke: der Rest

Wir haben, sagen wir mal, 334 Datensätze. Und wir legen die Anzahl der Datensätze pro Seite auf 10 fest. Das gibt 33 ganze Seiten a 10 Datensätze, und auf der letzten Seite haben wir nur noch vier Datensätze. Da muss man jetzt eine Sonderbehandlung für den Rest einbauen, weil unser Array ja nur einen Index bis 333 zuläßt – Offset by one, den 334. Array-Entrag gibt es nicht mehr. Dieses Dilemma hat man nicht, wenn man direkt auf die Datenbank zugreift und mit einem SQL-Limit arbeiten kann, da ist es egal wenn am Ende die Seite nicht ganz voll wird, es gibt keine Fehlermeldung.

Aber, wir arbeiten ja mit einem Array, und da muss man die ganze Logik ein bisschen umstricken. Also, frisch ans Werk!

Das Array

… enthält 334 Kochrezepte und sieht nach einem json_decode in etwa so aus:

array(1) 
{ ["rezepte"]=> array(334) {
 [0]=> array(3) { ["ID"]=> string(4) "1828" ["post_title"]=> string(31) "Pudding nach Art des Hauses Leu" ["post_content"]=> string(37) "Vanille oder Schoko oder Johannisbeer" }
 [1]=> array(3) { ["ID"]=> string(4) "1827" ["post_title"]=> string(5) "Milch" ["post_content"]=> string(23) "reicht nicht bis morgen" } 
 [2]=> array(3) { ["ID"]=> string(4) "1825" ["post_title"]=> string(14) "Petersilwurzel" ["post_content"]=> string(29) "hab ich abgeerntet, ist nicht" }

Das heißt, um beispielsweise an die ID und den Titel eines Datensatzes heranzukommen, muss ich folgende Syntax verwenden:

$akt_id = $dataObject['rezepte'][$i]['ID'];
$akt_titel = $dataObject['rezepte'][$i]['post_title'];

Das ist jetzt noch nicht weiter tragisch, ich muss nur daran denken dass mein Array bei 0 anfängt, das heißt bei einer Anzahl Datensätze pro Seite von X darf ich nur von 0 bis X-1 ausgeben. Praktisch sieht das z.B.bei 10 Datensätzen pro Seite so aus:

Seite 1: Datensätze 0…9
Seite 2: Datensätze 10…19
Seite 3: Datensätze 20…29

letzte Seite: der Rest

Das heisst aber, ich muss für meine ganzen Seiten eine andere Logik anwenden als für die letzte Seite, auf der auch weniger als 10 Datensätze stehen können.

Aber fangen wir erstmal vorne an:

Ermitteln der Seitenzahl und des Restes

Mein Array mit den 334 Datensätzen heißt $dataObject und wurde aus einem JSON in ein Array umgewandelt. Jetzt rechne ich erstmal aus, wieviele ganze Seiten das ergibt, wenn ich hier beispielsweise 10 Datensätze pro Seite ausgebe, und welcher Rest bleibt.

<?php

//JSON alle Rezepte abholen und in Array umwandeln
$json = file_get_contents('http://localhost/kleine_api/rezepte/rezeptelesen.php');
$dataObject = json_decode($json,true);

//Rezepte pro Seite ausrechnen
$pro_seite = 10; //Wert kann beliebig geändert werden
$anzahl = count($dataObject['rezepte']);
echo "Datens&auml;tze gesamt = ".$anzahl."<br>";
echo "Das macht ".floor($anzahl/$pro_seite)." ganze Seiten
 bei ".$pro_seite." Rezepten pro Seite, Rest: ".$anzahl%$pro_seite."<br>";

Ich habe mit Absicht eine sehr geschwätzige echo-Ausgabe eingebaut, zur besseren Kontrolle. Der Output sieht jetzt mal so aus:

echo_kennzahlen

echo_kennzahlen

Damit kann ich mir jetzt schon mal die Buttons für die einzelnen Seiten zusammenbauen. Das mache ich wieder mit einem Formular. Ich zähle mein i von Seite 1 bis zur letzten Seite inklusive hoch, als Value kriegt jeder Button den aktuellen Wert von i. Ausserdem kriegt jeder Button noch eine ID, die brauchen wir später noch.

//Formular mit Buttons für die Seitenzahlen, Start 1, Ende: nächsthöhere Ganzzahl
for ($i=1; $i <=ceil($anzahl/$pro_seite); $i++){
    echo "<form action = 'array_num_page.php' method = 'get'>";
    echo "<input type='submit' id='el_num_button_".$i."' name=akt_i value='".$i."'>";
}
echo "</form>";

Das sieht jetzt schonmal so aus:

buttons

buttons

Jetzt gehts daran zu ermitteln, welche Seite gerade aktuell ist. Ich starte als Default mit der 1, und je nachdem welchen Button der Anwender angeklickt hat hole ich mir den Wert aus dem Formular.

//Checken ob eine Seite geklickt wurde
$seitenstart=1;
if (isset($_GET['akt_i'])){
    $seitenstart = $_GET['akt_i'];
    }else{
    $seitenstart=1;
    }
//versteckte Ausgabe für farbige Markierung der aktuellen Seite mit Script    
echo "<div id ='seitenstart' style='display:none;'>".$seitenstart."</div>";

Meinen Seitenstart schreibe ich mir noch in eine versteckte Div, die brauche ich nachher für die farbige Markierung der aktuellen Seite.

Jetzt gehts erst mal an die Ausgabe.

Die ganzen Seiten

Hier darf ich nur bis zur letzten ganzen Seite hochzählen, die letzte Seite mit den weniger Datensätzen kommt später dran:

//Ausgabe aller ganzen Seiten von 1 bis zum letzten ganzzahligen Vielfachen
if (($seitenstart > 0) and ($seitenstart < floor($anzahl/$pro_seite))){

//Tabelle für Ausgabe aller Rezepte zusammenbauen
echo "<table border : 1px>";
echo "<th>ID</th><th>Titel</th><th>Content</th> 
/*Ausgabe als Tabelle*/

Jetzt wird es ein bisschen knifflig. Ich bleibe mal bei einem Wert pro Seite von 10 Datensätzen, es klappt aber auch mit jedem anderen Wert.

Auf Seite eins muss ich bei 0 anfangen und bis eins weniger als die Anzahl der Datensätze pro Seite hochzählen, also bis 9. Auf Seite zwei muss ich bei 10 anfangen und bis 19 hochzählen, usw.:

//Bei 0 anfangen, bis max. eins weniger als die Anzahl der ganzen Seiten hochzählen (0..9, 10..19 usw...)
for ($i=($seitenstart-1)*$pro_seite; $i <= $pro_seite*$seitenstart-1; $i++){

Die ganze for-Schleife mit der Ausgabe der einzelnen Werte aus dem Array in Tabellenzeilen sieht so aus:

//Bei 0 anfangen, bis max. eins weniger als die Anzahl der ganzen Seiten hochzählen (0..9, 10..19 usw...)
for ($i=($seitenstart-1)*$pro_seite; $i <= $pro_seite*$seitenstart-1; $i++){

echo "<tr>";
//Werte aus dem Array auf Var. legen
$akt_id = $dataObject['rezepte'][$i]['ID'];
$akt_titel = $dataObject['rezepte'][$i]['post_title'];
//Content tags raus und auf 100 Zeichen kürzen
$akt_content = substr(strip_tags($dataObject['rezepte'][$i]['post_content']),0,200);

echo "<td>".$akt_id."</td><td>".$akt_titel."</td><td>".$akt_content."</td>";
echo "</tr>";
}

Damit haben wir die Ausgabe aller ganzen Seiten erledigt. Jetzt kümmern wir uns um den Rest. Da nehmen wir nur noch die Indizes aus dem Array, die grösser als der letzte Wert vor dem letzten ganzzahligen Vielfachen der Seitenzahl sind, und marschieren nach oben durch bis 1 vor der Gesamtzahl der Datensätze.

//****Ausgabe Rest wenn Seitenzahl grösser als das letzte ganzzahlige Vielfache ist
if($seitenstart > floor($anzahl/$pro_seite)){
...

//starten beim letzten ganzzahligen Vielfachen der Gesamt-Seitenzahl, 
//stoppen eins unter Gesamtzahl
for ($i=floor($anzahl/$pro_seite)*$pro_seite; $i < $anzahl; $i++){

Voila, das wars! Der ganze Code mit Ausgabe der Tabelle sieht so aus:

//**************************Ausgabe Rest wenn Seitenzahl grösser als das letzte ganzzahlige Vielfache ist
if($seitenstart > floor($anzahl/$pro_seite)){
    

//Tabelle für Ausgabe des Rests zusammenbauen
echo "<table border : 1px>";
echo "<th>ID</th><th>Titel</th><th>Content</th>";

//starten beim letzten ganzzahligen Vielfachen der Gesamt-Seitenzahl, stoppen eins unter Gesamtzahl
for ($i=floor($anzahl/$pro_seite)*$pro_seite; $i < $anzahl; $i++){

echo "<tr>";
//Werte aus dem Array auf Var. legen
$akt_id = $dataObject['rezepte'][$i]['ID'];
$akt_titel = $dataObject['rezepte'][$i]['post_title'];
//Content tags raus und auf 100 Zeichen kürzen
$akt_content = substr(strip_tags($dataObject['rezepte'][$i]['post_content']),0,200);

echo "<td>".$akt_id."</td><td>".$akt_titel."</td><td>".$akt_content."</td>";
echo "</tr>";
}
echo "</table>";
    
}

Noch ein Javascript-Zuckerl: aktuelle Seite farbig markieren

Ich hab mir ja zu jedem Seitenzahl-Button eine ID generiert, das sah so aus:

echo "<input type='submit' id='el_num_button_".$i."' name=akt_i value='".$i."'>";

Und ich habe mir den Wert der aktuellen Seite (default 1) in eine versteckte div geschrieben:

//versteckte Ausgabe für farbige Markierung der aktuellen Seite mit Script    
echo "<div id ='seitenstart' style='display:none;'>".$seitenstart."</div>";

Damit habe ich mir ein kleines Javascripterl gebastelt, das die aktuelle Seite farbig markiert:

<script>

    $(document).ready(function() {
    var dieid = document.getElementById('seitenstart').innerHTML;
    //alert(dieid);
    document.getElementById('el_num_button_'+dieid).style.backgroundColor = "lightgreen";
    }); 


</script>

Ich klaube mir die aktuelle Seitenzahl aus der verstecken Div und spreche damit die zugehörige Button-ID an, die style ich mir nach Belieben, das war auch schon alles. Hübscher kleiner Effekt!

seite_farbig

seite_farbig

 

WordPress Beiträge: einfache nummerische Pagination

Dies ist als Ableger des vorigen Beitrags entstanden, eine simple nummerische Pagination der Rezepte ist auch nicht wesentlich schwieriger als die alphabetische Pagination und kann ganz ähnlich konstruiert werden. Ich mache das wieder mit einem Shortcode in einem Plugin. Zuerst wird die Anzahl der pro Seite auszugebenden Rezepte festgelegt und ermittelt, wie viele Seiten das werden:

function el_num_pagination(){

//Anzahl Rezepte pro Seite festlegen
$pro_seite = 15;
    
//Anzahl aller veröffentlichten Rezepte bestimmen
global $wpdb;

         $alleposts = $wpdb->get_results( "SELECT * from wp_posts 
         where post_type like 'post' and post_status like 'publish'");
        
        $gefunden = $wpdb->num_rows;
        echo $gefunden." Rezepte insgesamt<br>";
        
        //ceil: nächsthöhere Ganzzahl
        $anzahl_seiten = ceil($gefunden/$pro_seite);
        //Debug-Ausgabe
        echo "Das sind: ".$anzahl_seiten." Seiten bei ".$pro_seite." Rezepten pro Seite";

Dann baue ich mir das Formular mit den Buttons für die Seitenzahlen zusammen:

//Formular mit Buttons
echo "<form action = '#' method = 'post'>";

for ($i=1; $i <=$anzahl_seiten; $i++){
    echo "<input type='submit' id='el_num_button' name='".$i."' value='".$i."'>";
}
echo "</form>";

Die Buttons werden in der style.css noch ein bisschen hübscher formatiert, die müssen nur ein wenig breiter werden:

#el_num_button{
    height:30px;
    width:24px;
    padding:2px;
    border: 2px solid white;
    margin 2px;
    padding: 1px 1px 1px;
}

Dann laufe ich wieder durch alle Seitenzahlen durch und frage mit dem if(isset()) ab, ob eine Seitenzahl angeklickt wurde. Falls ja, wird die Ausgabefunktion mit zwei Parametern aufgerufen, der Nummer der aktuellen Seite und der Anzahl der Rezepte pro Seite:

for ($j = 1; $j <= $anzahl_seiten; $j++){    
        if (isset($_POST[''.$j.''])){
            
            return el_num_aufruf($j,$pro_seite);
        }
    }

In der Ausgabefunktion nutze ich die Tatsache, dass man dem SQL LIMIT einen Offset mitgeben kann. Dieser muss bei 0 (Null) auf der Seite 1 anfangen, auf Seite 2 ist er dann einmal die Anzahl der Beiträge pro Seite, auf Seite drei zweimal etc… deswegen die Konstruktion mit dem $akt_seite-1:

function el_num_aufruf($akt_seite, $aufderseite){
    
        
    global $wpdb;
        
        //Limit Offset eins weniger als aktuelle Seite
        $hilf = ($akt_seite-1)*$aufderseite;
        
        $alleposts = $wpdb->get_results( "SELECT * from wp_posts 
         where post_type like 'post' 
         and post_status like 'publish'
         LIMIT ".$hilf.", ".$aufderseite." ");
        $gefunden = $wpdb->num_rows;
        
        foreach ($alleposts as $einpost){
            
            $pfad = get_the_permalink($einpost->ID);
            echo "<a href = '".$pfad."'>".$einpost->post_title."</a><br>";
            
        }
    
} //end function el_num_aufruf

Am Ende gebe ich die Rezepte mit den Links wieder mit einem Foreach aus. Das wars!

15_pro_seite

15_pro_seite

Das funktioniert auch mit anderen Werten für die Ausgabe pro Seite, ich nehm mal 7:

7_pro_seite

7_pro_seite

Oder wesentlich mehr, 30, da sind wir komplett flexibel:

30_pro_seite

30_pro_seite

Man könnte jetzt natürlich noch ein Auswahlfeld für den Benutzer einbauen, so dass er selbst wählen kann wieviele Rezepte pro Seite er angezeigt haben möchte, aber damit kann sich jeder selber amüsieren. Ich werde eh die alfabetische Pagination nehmen, die taugt mir besser für meine Anwender.

WordPress Bewertungsformular mit AJAX

Einleitung

Sie kennen wahrscheinlich mein Bewertungsformular, es steht am Ende jedes Artikels und erlaubt eine Benotung von sehr gut bis ungenügend. Bislang ist es so, dass ich eine Nachricht erhalte, wenn jemand eine Bewertung abgeschickt hat, und die Information nur für mich privat relevant ist. Jetzt dachte ich mir, das geht auch anders: erstens möchte ich am Ende jedes Artikels angezeigt bekommen, ob schon Bewertungen vorhanden sind. Und zweitens soll sich die Anzeige automatisch aktualisieren, wenn jemand eine neue Bewertung abgibt. Dazu brauchts a bisserl AJAX, und das geht mit WordPress eigentlich ganz einfach, wenn man das Prinzip einmal überrissen hat.

Formular am Ende jedes Artikels anzeigen

Dafür basteln wir uns ein Plugin, und in das kommt als Erstes mal das Formular und ein Filter, der das ganze ans Ende jedes Contents hängt. Wahlweise kann man das auch in eine Abfrage klemmen, die das Formular nur in der Einzelansicht anzeigt, das fragt man mit dem if(is_single() ab. Sonst ist nichts weiter dabei. Ansonsten gibt es 6 Radiobuttons mit den Noten und einen Button zum absenden des Formulars.

function el_insertText($content) {
    echo $content."<br><hr>";
  // if(is_single()) {
      
      echo '<div style="border:1px dotted #000; text-align:center; padding:10px;">';
      echo '<h4>Dir gefällt dieser Beitrag?</h4>';
      echo '<p>Dann bewerte ihn!</p>';
      echo '</div>';
      
     echo " <form action = '' method='post'>";
  
      echo "<fieldset>
        <div id = 'noten' class = 'noten' style = 'border:2px solid blue; padding :4px;'>
        <input type='radio'  name='note' value='1'>sehr gut
        <input type='radio' name='note' value='2'>gut
        <input type='radio' name='note' value='3'>befriedigend<br>
        <input type='radio'  name='note' value='4'>ausreichend
        <input type='radio' name='note' value='5'>mangelhaft
        <input type='radio' name='note' value='6'>ungenügend
        
        <input type ='button' name = 'absenden' class = 'bew-button' value = 'abschicken'>
        </div>
      </fieldset>";
    echo "</form>";
          
  // } //End von is_single
  
}
add_filter ('the_content', 'el_insertText');

Was brauchen wir für die Verarbeitung?

Eine Tabelle zum Speichern der Noten

Erstens eine Tabelle, in der die Bewertungen gespeichert werden. Ich hab mir da eine kleine Lösung im PHPmyAdmin angelegt, die Tabelle heisst bewertungen und hat drei Felder, die id als Autowert, die beitrags_id und die note.

tabelle_bewertungen

tabelle_bewertungen

WordPress-Puristen schreien hier wahrscheinlich schon Zetermordio, man könnte natürlich die Bewertungen in die wp_options reinklemmen, aber das war mir jetzt zu umständlich, ich arbeite lieber mit einer eigenen Tabelle.

Die aktuelle Beitrags-ID

Zweitens brauchen wir die ID des bewerteten Beitrags, und drittens natürlich die vergebene Note. Die ID holt man sich mit get_the_ID(), und ich schreibe sie als unsichtbaren Text mit rein, das geht so:

echo "<span id = 'beitragsid' style='display:none;'>".get_the_ID()."</span><br>";

Dann kann ich mir nachher anhand der Span-id im Javascript die ID herauspflücken. Die Note hole ich mir auch mit dem Script, dazu gleich mehr.

Eine Logik zur Ausgabe der bereits vorhandenen Bewertungen

Drittens brauchen wir für die Anzeige der schon vorhandenen Bewertungen eine Abfrage auf unsere Tabelle bewertungen und eine Ausgabe der berechneten Durchschnittsnote sowie der Anzahl der vorhandenen Bewertungen. Das ist nicht weiter wild, mit $wpdb und der BeitragsID ist das fix erledigt.

global $wpdb;
    $alleposts = $wpdb->get_results( "SELECT * from bewertungen 
    where beitrags_id = ".get_the_ID()."");
    
    $gefunden = $wpdb->num_rows;
        
    $durchschnitt = 0;
    foreach($alleposts as $einpost){
        $durchschnitt = $durchschnitt + $einpost->note;
    }
    
    if ($gefunden != 0){        
    $durchschnitt = $durchschnitt / $gefunden;
    }
        
    $durchschnitt= round($durchschnitt);

Damit aus dem gerundeten Notenwert auch ein Wort wird, klemmen wir noch einen switch case dahinter:

    $wort = '';
switch ($durchschnitt) {
    case 0:
        $wort = "keine Bewertung";
        break;
    case 1:
        $wort = "sehr gut";
        break;
    case 2:
        $wort =  "gut";
        break;
    case 3:
        $wort = "befriedigend";
        break;
    case 4:
        $wort =  "ausreichend";
        break;
    case 5:
        $wort =  "mangelhaft";
        break;
    case 6:
        $wort =  "ungenügend";
        break;
}
    
    echo "<span id = 'ausgabe'>".$gefunden." Bewertungen gefunden, Durchschnitt: ".$wort."<span>";

Die Ausgabe-Span kriegt gleich noch eine id verpasst, hier schreiben wir nachher unsere aktualisierten Werte rein.

Das zugehörige Javascript

…kommt mit ins Plugin-Verzeichnis, und wird mit enqueue script geladen. Wir schicken mit dem wp_localize_script gleich noch die URL zur admin-ajax.php mit.

add_action( 'wp_enqueue_scripts', 'el_bew_assets' );
function el_bew_assets() {
    
        wp_enqueue_script( 'bew', plugins_url( '/el_bewertung.js', __FILE__ ), array('jquery'), '1.0', true );
        wp_localize_script( 'bew', 'bewertungen', array(
        'ajax_url' => admin_url( 'admin-ajax.php' )));

    }

Wir packen unser gesamtes Script in eine document-ready Function, weil sonst JQuery noch nicht geladen sein könnte, wenn wir unser Script ausführen.  Die Function rennt los, sobald jemand den Bewertungs-Button geklickt hat. Zuerst werden die benötigten Daten auf Variable gelegt, und dann noch abgecheckt ob auch eine Benotung anggeklickt wurde.

jQuery(document).ready(function($){
jQuery( document ).on( 'click', '.bew-button', function() {
            
            
            // Ajax url aus localize script
            var a_url = bewertungen.ajax_url;
                        
            //Note aus den Radiobuttons holen
            var selectedOption = $("input:radio[name=note]:checked").val();
            
            //Beitragsid aus span holen
            var akt_id = document.getElementById("beitragsid").innerHTML;
            
            //Check ob keine Note angewählt, Abbruch
            if (typeof selectedOption === 'undefined'){
            alert('Bitte eine Note wählen!');
            return;}
                
            var post_note = selectedOption;
            
            alert('Vielen Dank, ihre Benotung wurde gespeichert');
            
            //Ajax-Call        
            jQuery.ajax({
                url : a_url,
                type : 'post',
                data : {
                    action : 'bew_add_bewertung',
                    post_id : akt_id,
                    note : post_note
                },
                success : function( response ) {
                    document.getElementById("ausgabe").innerHTML = response;
                
                }
    });
            
})
});

Der AJAX-Call ist recht einfach, es wird die ajax-admin-URL mitgegeben und als Request-Typ ‚post‘ definiert. In den data schicken wir zuerst mit, welche Funktion ausgeführt werden soll (dazu gleich mehr), und übergeben die Beitrags-ID sowie die Note. In der success-Function schreiben wir den Rückgabewert des Calls in die Span mit der ID ausgabe. Das wars schon!

Jetzt müssen wir WordPress nur noch anweisen, die Callback-Function auch zuzuordnen, dazu ergänzen wir unser Plugin um folgende Zeilen:

add_action( 'wp_ajax_nopriv_bew_add_bewertung', 'bew_add_bewertung' );
add_action( 'wp_ajax_bew_add_bewertung', 'bew_add_bewertung' );

Das erste Argument mit dem nopriv gilt fürs Frontend, das Zweite könnte ich mir eigentlich sparen, das wäre für die Admin-View und steht hier nur der Vollständigkeit halber.

Die Callback-Funktion

Macht ganz etwas Ähnliches wie unsere Bildschrimausgabe von oben, sie ermittelt anhand der übergebenen Beitrags-ID und der Note die aktuellen Zahlen. Dafür wird zuerst einmal die neue Bewertung in die Tabelle bewertungen eingetragen (Insert), dann wird neu berechnet und ausgegeben. Den switch case könnte man auch in eine eigene Funktion auslagern, ich habs mit copy&paste gemacht, ist genau dasselbe wie oben.

function bew_add_bewertung(){
    
    global $wpdb;
    
    //Neue Benotung eintragen
    $wpdb->insert('bewertungen', array(
            'beitrags_id' => $_POST['post_id'],
            'note' => $_POST['note']
            ));
            
    //gtNeue Anzahl holen
        $alleposts = $wpdb->get_results( "SELECT * from bewertungen 
    where beitrags_id = ".$_POST['post_id']."");
    
    $anzahl = $wpdb->num_rows;
    $durchschnitt = 0;
    foreach($alleposts as $einpost){
        $durchschnitt = $durchschnitt + $einpost->note;
    }
    
    if ($anzahl != 0){        
    $durchschnitt = $durchschnitt / $anzahl;
    }
    
    $durchschnitt= round($durchschnitt);
     $wort='';
    switch ($durchschnitt) {
    case 0:
        $wort = "keine Bewertung";
        break;
    case 1:
        $wort = "sehr gut";
        break;
    case 2:
        $wort =  "gut";
        break;
    case 3:
        $wort = "befriedigend";
        break;
    case 4:
        $wort =  "ausreichend";
        break;
    case 5:
        $wort =  "mangelhaft";
        break;
    case 6:
        $wort =  "ungenügend";
        break;
}
    
    $rueckgabe = $anzahl." Bewertungen, Durchschnittsnote: ".$wort;    
    echo $rueckgabe;
    
    die();
    
    
} //ende function bew_add_bewertung

Das die() am Ende braucht WordPress, um den Call korrekt auszuführen. Aber damit sind wir auch schon fertig. Die bereits vorhandenen Bewertungen werden angezeigt:

bewertungformular1

bewertungformular1

Wenn jetzt ein Besucher eine Bewertung abgibt, wird die Anzeige nach Klick auf den absenden-Button automatisch aktualisiert:

bewertungsformular2

bewertungsformular2

Man könnte an der Optik noch ein bisschen was verbessern, ich gebs ja zu, aber funktionieren tut die Sache einwandfrei. Wie gesagt, wenn man sich mit der WordPress-eigenen Ajax-Logik einmal auseinandergesetzt hat, ist so etwas nicht wirklich schwierig zu realisieren.

WordPress Stichwortsuche mit Treffer-Highlighting

Habt ihr euch auch schon manchmal gewünscht, WordPress würde die Suchergebnisse irgendwie kennzeichnen – zum Beispiel durch eine farbige Hinterlegung der gefundenen Begriffe? Manchmal ist es nämlich gar nicht so leicht herauszufinden, wo im Text sich der gesuchte Begriff (oder die gesuchten Begriffe) verstecken. Das eine durchaus übliche Sache, und ich hab mir mal ein paar Gedanken zu dem Thema gemacht und mich im Inselfisch-Kochbuch an die Umsetzung gemacht. Dabei ist ein Plugin herausgekommen, das sicher noch ein bisschen Optimierungspotential hat, aber so im Prinzip funktioniert die Sache. Wurde eh schon lang wieder einmal Zeit für ein bisschen Spaß auf der Datenbank!

Die Anforderungen

  • Es sollen ein oder mehrere Suchbegriffe (durch Leerzeichen getrennt) eingegeben werden können.
  • Zuerst sollen alle Rezepte ausgegeben werden, bei denen die gesuchten Begriffe schon im Titel vorkommen. Die Treffer werden farbig hinterlegt.
  • Dann sollen noch alle Rezepte nachfolgen, bei denen die Suchbegriffe im Rezepttext = post content vorkommen, auch hier farbig hinterlegt
  • Durch Klick auf den Rezepttitel soll man direkt zum Rezepttext gelangen.

Wir fangen natürlich mit einem Formular an

Das Plugin fängt mit einem einfachen Eingabefeld an:

/*
Plugin Name: Evis Suche mit Highlighting
Plugin URI: http://localhost/wp_ajax/wp-content/plugins/suche
Description: Eigenes Suchmodul mit Highlighting
Version: 1.0
Author: Evi Leu
Author URI: http://www.evileu.de
*/

//Formular als Shortcode einbinden
 function suche_form(){
     
    echo "<h3>Rezeptsuche mit Highlighting</h3>";
    echo "<p>Mehrere Stichworte mit Leerzeichen trennen</p>";
    
    
    echo "<form action='#' method='post'>";
    echo "<input type='text' size='100' name = 'suchstring' >";
    echo "</form>";
    

Der Inhalt des Textfeldes wird übernommen, wenn der Anwender auf Return drückt. Dann legen wir los:

if (isset($_POST['suchstring'])){
    
    //***ein oder mehrere Leerzeichen im String?
    if (strpos($_POST['suchstring']," ")>0){
        
        $suchsql = "";
        $suchsql_content = "";
        //string am leerzeichen trennen
        $strings = explode(" ",$_POST['suchstring']);
        $anzahl = count($strings);
        $ersterstring = $strings[0];
        for ($i=1; $i < $anzahl; $i++){
            //suchstring für post_title zusammenbauen
            $suchsql = $suchsql." AND post_title like '%".$strings[$i]."%' ";
            //suchstring für post_content zusammenbauen
            $suchsql_content = $suchsql_content." AND post_content like '%".$strings[$i]."%' ";
        }    
    }
    //***kein Leerzeichen im String
    else{$ersterstring = $_POST['suchstring'];
        
    }

Was hab ich gemacht? Wenn ein oder mehrere Leerzeichen im Suchstring vorhanden sind, zerlege ich den String mit explode() in ein Array. Dann baue ich mir aus allen vorkommenden Einzelstrings ein SQL-Snippet zusammen, eins für die Abfrage des post_title und eins für die Abfrage des post_content. Man kann die AND-Klauseln einfach hintereinanderhängen, es sollen ja alle Suchstrings gefunden werden. Groß/Kleinschreibung ist egal, der MySQL-Like ist nicht case sensitive. Wenn kein Leerzeichen im Suchbegriff vorkommt, übernehme ich ihn im Ganzen.

Die erste Abfrage geht auf den Titel

Mit dem zusammengebauten SQL-Snippet von oben starte ich die erste Abfrage: Dann gebe ich erstmal aus, wieviele Rezepte mit den Suchbegriffen im Titel gefunden wurden.

global $wpdb; 

    
    /******Suchstring im Titel?****/
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts 
where post_title like '%".$ersterstring."%' ".$suchsql."
    and post_type like 'post' and post_status like 'publish' order by post_title");
    
    $gefunden = $wpdb->num_rows;
    if ($gefunden == 1){
        
        echo $gefunden." Rezept mit ".$_POST['suchstring']." im Titel:";
    }
    if ($gefunden > 1){
        
        echo $gefunden." Rezepte mit ".$_POST['suchstring']." im Titel:";
    }

Dann klappere ich mit einem foreach alle Ergebnisse des ersten Selects ab und mache mit PHP ein paar Ersetzungen für das Highlighting der Suchbegriffe. Dafür benutze ich den str_ireplace, der ist nicht case sensitive. Dabei geht der erste Suchebegriff extra, für alle weiteren steppe ich mit der for-Anweisung durch unser Array von oben.

foreach($alleposts as $einpost){
        
        
        //**** ersten Suchstring im Titel highlighten
        $replace = '<span style="background-color: #FF0;">' . $ersterstring. '</span>'; // create replacement
        $content = str_ireplace( $ersterstring, $replace, $einpost->post_title );
        //***** ersten Suchstring im content highlighten
        $rezepttext = str_ireplace( $ersterstring, $replace, $einpost->post_content );
        
        //****alle weiteren Suchstrings highlighten
        for ($j=1; $j<$anzahl;$j++){
            
            $replace = '<span style="background-color: #FF0;">' . $strings[$j]. '</span>'; // create replacement
            $content = str_ireplace( $strings[$j], $replace, $content );
            $rezepttext = str_ireplace( $strings[$j], $replace, $rezepttext );
        }
        //als Accordeon ausgeben
        echo "<details><summary>".$content."</summary>";
        echo $rezepttext."</details>";
        echo "<hr>";
            
    }

Ganz am Ende benutze ich eine HTML5-Syntax für die Ausgabe des Rezepttitels als anklickbares Detail/Summary Element, auch Accordeon genannt. Das sieht dann so aus:

accordeon

accordeon

Wenn man auf das Dreieckssymbol klickt, wird der Rezepttext eingeblendet — mit Highlighting:

schweinefilet_highlighted

schweinefilet_highlighted

Das funktioniert auch mit mehreren Stichworten (es können beliebig viele sein), ich hab hier mal „butter tomate“ eingegeben:

butter_tomate

butter_tomate

Und jetzt das ganz nochmal auf den Content:

Der SQL ist nur minimal verändert und geht jetzt auf den post_content, das Highlighting funktioniert genauso wie oben, auch die Ausgabe als Accordeon:

//*******Suchstring im Content */
    
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts where 
         post_content like '%".$ersterstring."%' ".$suchsql_content." 
    and post_type like 'post' and post_status like 'publish' order by post_title");
    
    foreach($alleposts as $einpost){
        
        //**** ersten Suchstring highlighten
        $replace = '<span style="background-color: #FF0;">' . $ersterstring. '</span>'; // create replacement
        $rezepttext = str_ireplace( $ersterstring, $replace, $einpost->post_content );
        
        //****alle weiteren Suchstrings highlighten
        for ($j=1; $j<$anzahl;$j++){
            
            $replace = '<span style="background-color: #FF0;">' . $strings[$j]. '</span>'; // create replacement
            $rezepttext = str_ireplace( $strings[$j], $replace, $rezepttext );
        }
        //als Accordeon ausgeben
        echo "<details><summary>".$einpost->post_title."</summary>";
        echo $rezepttext."</details>";
        echo "<hr>";
        //}
        
    }

Das wars schon! Jetzt könnte man höchstens noch anmeckern, dass manche Rezepte zweimal auftauchen, weil sie einmal mit der Suche im Titel und ein zweites Mal mit der Suche im Content gefunden wurden, aber auch dafür sollte es eine Lösung  geben, das ist Feintuning.

Viel Spaß beim Nachbauen! Hier noch ein Beispiel, dass es auch mit mehreren Stichworten funktioniert, ich habe „butter tomate olivenöl hackfleisch knoblauch“ eingegeben:

5_suchworte

5_suchworte

Bemerkung nach Testbetrieb: immer diese Umlauts

Darauf bin ich erst später gekommen: der MySQL-Like ignoriert in der Kollation utf8_general_ci die deutschen Umlaute. So wird zum Beispiel zum Stichwort „huhn“ auch „hühnchen“ gefunden, da ü wird als u behandelt. Das wäre ja soweit noch nicht weiter tragisch, aber der PHP str_ireplace() macht sehr wohl einen Unterschied zwischen Umlauten und aeoui, da fällt das Highlighting auf die Nase… na ja, ich finde das jetzt nicht wirklich schlimm, es ist halt ein kleines Manko. Deswegen jetzt mit der Datenbankkollation rumzuexperimentieren erspare ich mir, das wäre mit Kanonen auf Spatzen geschossen.

Doppelt vorkommende Rezepte ausblenden

Bislang kamen Rezepte, die sowohl im Titel als auch im Content die eingegebenen Begriffe enthielten, doppelt vor. Das läßt sich relativ einfach unterbinden, ich merke mir einfach in einem Array, welche Rezept-IDs bei der Stichwortsuche im Titel schon gefunden wurden, und baue daraus einen String für eine not-in-Klausel. Dafür kommt noch folgender Code nach dem ersten SQL rein:

//merken, welche post ids schon gefunden wurden
    $merken = array();
    
    //Falls ein oder mehrere Rezepte gefunden wurden, IDs merken
    if ($gefunden>0){
        
            $k=0;
            foreach ($alleposts as $einpost){
            $merken[$k] = $einpost->ID;
            $k=$k+1;
            }
        }else{
            //Falls kein Rezept gefunden wurde, Dummy Eintrag mit ID 0
            $gemerkt = "(0)";
            }
    
    //Klammer für den not in aufbauen
    $gemerkt = "(";
    for ($l=0; $l < $gefunden; $l++){
   
    $gemerkt = $gemerkt.$merken[$l].", ";
    
    }
    //Klammer für den "not in" mit Dummy-Eintrag 0 abschliessen
    $gemerkt = $gemerkt."0)";

Den Dummy-Eintrag mit der 0 brauchts, damit der „not in“ nicht auf eine leere Klammer rennt und gar nichts ausgibt. Mit dem jetzt in der Variable $gemerkt gespeicherten String mit den IDs gehe ich in den zweiten SQL, der im Content sucht, und füge noch eine „not in“-Klausel  hinzu:

$alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts where 
         post_content like '%".$ersterstring."%' ".$suchsql_content." 
    and post_type like 'post' and post_status like 'publish' 
and ID not in ".$gemerkt." order by post_title");

Das filtert alle Rezepte heraus, die bereits in der ersten SQL-Abfrage gefunden wurden.

 

HTML5 Datalist Value – ganz so einfach ist es nicht

Eigentlich gibt es in HTML ja die Option, einem Dropdownfeld zusätzlich zur Bezeichnung einen Value mitzugeben, der dann bei der Auswahl weitergegeben wird. In meinem Fall wäre die Bezeichnung der Titel des Rezeptes, und der Value die ID aus der Datenbank. Das würde dann etwa so aussehen:

...
<option value = '345'>Dampfnudeln wie bei Oma</option>"
...

Sobald der Benutzer den Eintrag Dampfnudeln ausgewählt und auf Return gedrückt hat, wird der Wert 345 übergeben – sollte zumindest. Diese Option funktioniert aber bei einer Datalist browserabhängig ganz unterschiedlich, wäre ja zu einfach.

In Chrome

…kriege ich die numerischen IDs in der Auswahlliste mit angezeigt, das ist nicht besonders schön, funktioniert aber wenigstens.

chrome_value

chrome_value

Es wird auch richtig bei der Auswahl die ID weitergegeben.

In Firefox Quantum

wird der Value-Wert einfach ignoriert, ich bekomme nur die Titel angezeigt und bei der Auswahl ausgegeben.

firefox_value

firefox_value

Im Internet Explorer 10

wirds besonders g’schmackig, da kann man gar nicht mehr nach dem Titel auswählen, der Autocomplete funktioniert erst wieder, wenn ich den Option Value ganz herausnehme.

Was nun? Ich mach mal kurzen Prozeß

Bevor ich mir hier für die unterschiedlichen Varianten der einzelnen Browserausgaben einen Wolf programmiere, verzichte ich ganz auf die Übergabe der ID und suche meine Rezepte schlicht nach dem Titel aus der Datenbank. Das kann ich mir erlauben, weil a) ich nur ein-eindeutige Titel habe und b) die Schreibweise aus dem Dropdownfeld identisch ist mit der Schreibweise in der wp_posts. Falls der User dann noch zusätzliche Zeichen eingibt (woran ihn nieman hindert) wird halt kein Rezept gefunden, aber damit kann ich leben. Um c) auszuschliessen, dass Revisions oder attachments mit reinrutschen, erweitere ich meinen Select einfach um post_type like ‚post‘ and post_status like ‚publish‘, aber das reicht dann.

Das Ganze klemme ich in eine if(isset($_POST)…Abfrage und packe es in den Shortcode für das Formular mit rein, das sieht dann so aus:

//*********Formular als Shortcode einbinden
 function asuche(){ 
    echo "<form action='#' method='post'>";
    echo "<input type = 'text' name = 'ausgesucht' size = '100' id = 'textfeld' autocomplete = 'off' onkeyup = 'myFunction(this.value);' list = 'suchliste'>";
    
    echo "<datalist id='suchliste'>";
    echo "<div id = 'liste'>";
    echo "</div>";    
    echo "</datalist>";
    echo "</form>";
     if (isset($_POST['ausgesucht'])){
             
    /*********Action*/     
    global $wpdb;
    $alleposts = $wpdb->get_results( "SELECT * from ".$wpdb->prefix."posts 
where post_title like '".$_POST['ausgesucht']."'
 and post_status like 'publish' and post_type like 'post'");
    foreach($alleposts as $einpost){
        
        echo "<h1>".$einpost->post_title."</h1>";
        echo $einpost->post_content;
    }
    /*********End Action*/
    }    
    
 }
 add_shortcode('asuche', 'asuche');

Damit wird folgende Funktionalität realisiert: der User kann aus der Autocomplete-Liste einen Eintrag auswählen und mit Return in das Textfeld übernehmen. Bei nochmaligem Drücken von Return wird der komplette Text des ausgewählten Rezeptes angezeigt. Das ist mir schön genug, und eingängig in der Bedienung.

Nur der Internet-Explorer macht noch Zicken, der schlägt beharrlich nur Rezepte vor, die mit dem eingegebenen Suchbegriff anfangen, nicht alle in denen der Begriff irgendwo im Titeltext vorkommt. Sei es drum, in Chrome und Firefox funktioniert es jedenfalls wie beabsichtigt.