PHP/Suchmaschine
Das folgende Datenbankprojekt basiert auf der Anwendung phpHoo, die gut für die eigene Homepage eingesetzt werden kann. Das ist weitaus eleganter und flexibler als die ewig langen und veralteten Linksammlungen.
Die vorgestellte Suchmaschine besteht aus drei Dateien:
- search.php
- main.php
- phpHoo.css
Erzeugung von Testdaten
Die vorliegende Lösung basiert auf zwei Tabellen innerhalb der bestehenden Datenbank "php5test". Die Tabelle "categories" speichert die Kategorien der im Verzeichnis gespeicherten Seiten. Durch einen Verweis auf sich selbst kann eine beliebig tiefe Hierarchie in einer Tabelle gehalten werden. In jeder Kategorie können, wie bei einem Verzeichnis üblich, beliebig viele Links stehen; diese werden in der Tabelle "links" erfasst.
## Name phphoo.sql
##
CREATE TABLE categories(
catid bigint(21) NOT NULL auto_increment,
catname varchar(50) NOT NULL,
catparent bigint(21),
PRIMARY KEY(CatID),
UNIQUE (catname));
CREATE TABLE links(
linkid bigint(21) NOT NULL auto_increment,
catid bigint(21) NOT NULL,
url varchar(255) NOT NULL,
linkname varchar(255) NOT NULL,
description varchar(255),
submitdate date,
submitname varchar(80),
submitemail varchar(80),
approved tinyint,
PRIMARY KEY (LinkID),
UNIQUE(URL));
## Beispieldaten
## Vier Kategorien
INSERT INTO categories (catname) VALUES ("Meine Shops");
INSERT INTO categories (catname) VALUES ("Banken");
INSERT INTO categories (catname) VALUES ("Musik");
INSERT INTO categories (catname) VALUES ("Computer");
## Drei Unterkategorien der Kategorie "2" (Banken)
INSERT INTO categories (catname, catparent) VALUES ("Credit Suisse", 2);
INSERT INTO categories (catname, catparent) VALUES ("UBS", 2);
INSERT INTO categories (catname, catparent) VALUES ("Migrosbank", 2);
|
Die Stammfunktionen
Zum Schreiben einer effektiven Anwendung, sind einige Funktionen notwendig, die später ein hohes Mass an Flexibilität erlauben. Die Anwendung soll deshalb objektorientiert arbeiten. Definiert werden zwei Klassen:
- MySQL wird in der Datei "search.php" definiert.
- phpHoo erbt von "MySQL" und wird in der Datei "main.php" definiert.
Zuerst erfolgt die Definition der Klasse "MySQL" in der Datei "search.php". Der erste Teil erzeugt einige globale Variablen.
<?php
## Name search.php
class MySQL
{
## Konfiguration
const DATABASE = "php5test";
const USER = "root";
const PASSWORD = "";
const SERVER = "localhost";
private $CONNECTION = "";
private $TRAIL = array();
private $HITS = array();
private $AUTOAPPROVE = true;
## Anzeige von Datenbankfehlern
private function error($text)
{
$no = mysql_errno();
$msg = mysql_error();
return "[$text] ( $no : $msg )<br />\n";
}
## Der Konstruktor initialisiert Objekte der Klasse. Er kann eine
## Ausnahme erzeugen, die im Hauptprogramm mit try/catch abgefangen
## werden kann
public function __construct()
{
$user = self::USER;
$password = self::PASSWORD;
$server = self::SERVER;
$database = self::DATABASE;
$connection = @mysql_connect($server, $user, $password);
if (!$connection)
{
throw new Exception($this->error("Verbindungsfehler"));
}
if (!mysql_select_db($database, $connection))
{
throw new Exception($this->error("Datenbankfehler"));
}
$this->CONNECTION = $connection;
return true;
}
## Zugriff auf die privaten Eigenschaften
public function __get($prop)
{
switch ($prop)
{
case "Trail":
return $this->TRAIL;
break;
default:
echo "FAIL: $prop";
}
}
##### MySQL Methoden #####
## Abfrage mit "SELECT" wird häufiger benötigt
private function select($sql = "", $column = "")
{
## Zuerst werden Fehler abgefangen:
## Erkennung leerer Befehlsübergaben
if (empty($sql))
{
return false;
}
## Prüfung, ob der Befehl mit der Zeichenfolge "SELECT" beginnt
if (!preg_match("|^select|i", $sql))
{
throw new Exception("Unzulässiger Funktionsaufruf");
}
## Bei fehlender Verbindung soll nichts geschehen
if (empty($this->CONNECTION))
{
return false;
}
## Bei vorhandener Verbindung wird diese in eine lokale Variable
## übertragen
$connection = $this->CONNECTION;
## Direkte Ausführung der Abfrage, das Ergebnis wird in ein
## Abfrageobjekt überführt.
$results = @mysql_query($sql, $connection);
## War nichts zu holen, ist die Methode beendet.
if ((!$results) or (empty($results)))
{
@mysql_free_result($results);
return false;
}
## Sind Daten vorhanden, so werden diese nun reihenweise in ein
## Array geholt.
$count = 0;
$data = array();
while ($row = mysql_fetch_array($results))
{
$data[$count] = $row;
$count++;
}
## Abschliessend wird der für die Ergebnisliste verwendete
## Speicher freigegeben und das Array zurückgegeben.
mysql_free_result($results);
return $data;
}
## Mit der Methode "insert" werden Daten hinzugefügt. Die Methode
## gibt die ID-Nummer des eingefügten Datensatzes zurück.
function insert($sql = "")
{
if (empty($sql))
{
return false;
}
if (!preg_match("|^insert|i", $sql))
{
throw new Exception($this->error("Unzulässiger Funktionsaufruf"));
}
if (empty($this->CONNECTION))
{
return false;
}
$connection = $this->CONNECTION;
$results = @mysql_query($sql, $connection);
if (!$results)
{
throw new Exception($this->error("Datenbankfehler"));
}
$results = mysql_insert_id();
return $results;
}
##### phpHoo Methoden #####
## Ermittlung aller Kategorien unterhalb einer angegebenen Kategorie.
## Damit wird später der Mausklick von einer höheren zu einer niedrigeren
## Ebene realisiert.
public function getCategories($catparent = "")
{
## Da ein Vergleich mit "NULL" nicht zulässig ist, erfolgt zuerst
## eine entsprechende Abfrage.
if (empty($catparent))
{
$catparent = "ISNULL(catparent)";
}
else
{
$catparent = "catparent = $catparent";
}
## Erstellen einer SQL-Anweisung und Ausführung mit der Methode "select".
$sql = "SELECT catid, catname FROM categories WHERE $catparent ORDER BY catname";
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
return $results;
}
## Methode zum Durchlaufen des gesamten Kategorienbaums. Sie ist als "protected"
## gekennzeichnet, weil sie nur in der abgeleiteten Klasse "phpHoo" benutzt
## werden soll.
protected function getParentsInt($catid = "")
{
if (empty($catid))
{
return false;
}
unset($this->TRAIL);
$this->TRAIL = array();
$this->getParents($catid);
}
## Die folgende Methode wird nur von "getParentsID" aufgerufen, nie direkt. Sie
## ist deshalb als "private" gekennzeichnet.
private function getParents($catid = "")
{
if ((empty($catid)) or ("$catid" == "NULL"))
{
return false;
}
$sql = "SELECT catid, catparent, catname FROM categories WHERE catid = $catid";
$connection = $this->CONNECTION;
$results = mysql_query($sql, $connection);
if ((!$results) or (empty($results)))
{
mysql_free_result($results);
return false;
}
while ($row = mysql_fetch_array($results))
{
$trail = $this->TRAIL;
$count = count($trail);
$trail[$count] = $row;
$this->TRAIL = $trail;
$id = $row["catparent"];
$this->getParents($id);
}
return true;
}
## Diese Methode sucht die passende ID, wenn der Nutzer nach einem
## Namen sucht.
public function getCatIDFromName($catname = "")
{
if(empty($catname))
{
return false;
}
$sql = "SELECT catid FROM categories WHERE catname = \"$catname\"";
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
if (!empty($results))
{
$results = $results[0]["catid"];
}
return $results;
}
## Folgende Methode ermittelt anhand der ID den Namen.
function getCatNames($catid = "")
{
if($catid == 0)
{
return "Top";
}
$single = false;
if (!empty($catid))
{
$single = true;
$catid = "WHERE catid = $catid";
}
$sql = "SELECT catname FROM categories $catid";
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
if ($single)
{
if (!empty($results))
{
$results = $results[0]["catname"];
}
}
return $results;
}
## Diese Methode ermittelt die Hyperlinks innerhalb einer Kategorie.
public function getLinks($catid = "")
{
if (empty($catid))
{
$catid = "= 0";
}
else
{
$catid = "= $catid";
}
$sql = "SELECT url, linkname, description
FROM links
WHERE (approved != 0) AND catid $catid ORDER BY url";
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
return $results;
}
## Wird nur ein Link eingegeben, so kann umgekehrt auch die richtige
## Kategorie ermittelt werden.
function getCatFromLink($linkid = "")
{
if (empty($linkid))
{
return false;
}
$sql = "SELECT catid FROM links WHERE linkid = $linkid";
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
if (!empty($results))
{
$results = $results[0]["catid"];
}
return $results;
}
## Die folgende Suchfunktion bringt mehr Komfort für den Nutzer.
function search ()
{
## Prüfung, ob das Feld im Suchformular ausgefüllt wurde.
if(empty($_POST['Keywords']))
{
return false;
}
$keywords = $_POST['Keywords'];
$keywords = trim(urldecode($keywords));
## Mehrere Leerzeichen werden zu einem zusammengefasst.
$keywords = preg_replace("|(\s+)|", " ", $keywords);
## Sind mehrere Wörter vorhanden, so werden diese in ein Array
## extrahiert.
if(!preg_match("|\s|", $keywords))
{
$KeyWords[0] = "$keywords";
}
else
{
$KeyWords = explode(" ",$keywords);
}
## Vorbereitung einer SQL-Anweisung
$sql = "SELECT DISTINCT linkid, catid, url, linkname, description
FROM links
WHERE (approved != 0) AND ( ";
$count = count($KeyWords);
## Falls nur ein Wort angefragt wurde, so wird die entsprechende
## WHERE-Bedingung mit LIKE fertiggestellt.
if( $count == 1)
{
$single = $KeyWords[0];
$sql .= " (description LIKE \"%$single%\") OR
(linkname LIKE \"%$single%\") OR
(url LIKE \"%$single%\") ) ORDER BY linkname ";
}
## Ansonsten wird für jedes Wort eine Abfrage erzeugt und mit "OR"
## verknüpft.
else
{
$ticker = 0;
while (list($key, $word) = each ($KeyWords))
{
$ticker++;
if (!empty($word))
{
if ($ticker != $count)
{
$sql .= " ((description LIKE \"%$word%\") OR
(linkname LIKE \"%$word%\") OR
(url LIKE \"%$word%\")) ";
}
else
{
## Last condition, omit the trailing OR
$sql .= " ((description LIKE \"%$word%\") OR
(linkname LIKE \"%$word%\") OR
(url LIKE \"%$word%\")) ";
}
}
}
$sql .= " ) ORDER BY linkname";
}
## Abschliessend wird die SQL-Anweisung ausgeführt.
try
{
$results = $this->select($sql);
}
catch (Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
return $results;
}
## Mit dieser Methode werden neue Vorschläge in die Suchmaschine
## eingetragen.
public function suggest ()
{
if (count($_POST) == 0)
{
return false;
}
## Erfassung der Formulardaten aus dem Array "$_POST".
$catid = $_POST["catid"];
$url = $_POST["url"];
$description = $_POST["description"];
$linkname = $_POST["linkname"];
$submitname = $_POST["submitname"];
$submitemail = $_POST["submitemail"];
$submitdate = time();
## Verschiedene Fehlerabfragen: Leere Felder werden nicht
## eingetragen. Hier können weitere Prüfungen auf Sinnfälligkeit
## implementiert werden.
if (empty($url)) { return false; }
if (empty($description)) { return false; }
if (empty($linkname)) { return false; }
if (empty($submitname)) { return false; }
if (empty($submitemail)) { return false; }
## Mit "$approved" kann der Eintrag bis zu einer Prüfung gesperrt werden.
## Standardmässig ist "$this->AUTOAPPROVE" auf "TRUE" eingestellt, Links
## erscheinen deshalb sofort. Hier kann bei Bedarf eine leistungsfähigere
## Lösung implementiert werden.
$approved = 0;
if ($this->AUTOAPPROVE) { $approved = 1; }
## Erzeugen des Datensatzes.
$sql = "INSERT INTO links ";
$sql .= "(catid, url, linkname, description, submitname, submitemail, submitdate, approved) ";
$sql .= "VALUES ";
$sql .= "($catid, \"$url\", \"$linkname\", \"$description\", \"$submitname\", \"$submitemail\", $submitdate, $approved) ";
try
{
$results = $this->insert($sql);
}
catch(Exception $ex)
{
echo "<b>{$ex->getMessage()}</b>";
$results = FALSE;
}
return $results;
}
}
?>
|
Die Benutzeroberfläche
Als nächstes werden mit Hilfe der Klasse "MySQL" die Skripte erstellt, mit denen die eigentliche Nutzeroberfläche gestaltet wird.
<?php
## Name main.php
## Einbinden der bereits bestehenden Datei "search.php".
include("search.php");
## Erstellen der Klasse "phpHoo", welche die Grundfunktionen der Oberfläche
## ausführt. Sie erbt von der Klasse "MySQL", sodass die Datenbankzugriffe
## unmittelbar zur Verfügung stehen.
class phpHoo extends MySQL
{
## Der Konstruktor übernimmt die Instanziierung der Grundklasse
public function __construct()
{
try
{
parent::__construct();
}
catch(Exception $ex)
{
echo "Fehler: {$ex->getMessage()}";
}
}
## Folgende Funktion erstellt den Suchpfad durch das Verzeichnis und
## erzeugt die Links zur Navigation. So kann der Benutzer zum einen
## den Pfad zur Kategorie sehen und zum anderen jede Verzeichnisebene
## direkt erreichen.
public function breadCrumbs($catid = "")
{
if (empty($catid))
{
return;
}
$this->getParentsInt($catid);
$path = $this->Trail;
if(!empty($path))
{
while (list($key, $val) = each ($path))
{
$trail= sprintf(" | <a href=\"%s?viewcat=%s\"><b>%s</b></a> %s",
$_SERVER['PHP_SELF'],
stripslashes($val["catid"]),
stripslashes($val["catname"]),
isset($trail) ? $trail : '');
}
}
else
{
$trail = "";
}
return $trail;
}
## Die Anzeige der Seite beginnt immer mit der folgenden Funktion.
public function startPage($catid = "", $title = "", $msg = "")
{
## Erzeugen des Seitenkopfs. Hier kann auch ein eigenes Logo
## eingebaut werden.
echo <<<KOPFA
<html>
<head>
<title>phpHoo - $title</title>
<link rel="stylesheet" type="text/css" href="phpHoo.css">
</head>
<body bgcolor="#EEEEEE" text="#000000" link="#0033FF" vlink="#660099">
<center><h1>phpHoo</h1></center>\n
<div class=text>
KOPFA;
## Falls es bbei einer vorherigen Aktion eine Nachricht gab, so
## wird diese hier ausgegeben.
if (!empty($msg))
{
echo "<center><b>$msg</b></center>\n";
}
## Erzeugen des Suchformulars. Es enthält ein verstecktes Feld mit
## dem Namen "action", mit dessen Wert "search" bei der Auswertung
## erkannt wird, dass der Benutzer eine Suchanfrage gestartet hat.
echo <<<KOPFB
<center><form action="{$_SERVER['PHP_SELF']}" methode="post">
<input type="hidden" name="action" value="search">
<input type="text" name="Keywords" size="20">
<input type="submit" name="Search" VALUE="Search">
</form></center>
<h3><a href="{$_SERVER['PHP_SELF']}">Top</a>
KOPFB;
## Ausgabe des Pfads, der zur Navigation im Verzeichnis dient.
$trail = $this->breadCrumbs($catid);
echo "$trail</h3>\n<hr>\n";
return;
}
## Folgende Funktion durchsucht alle Kategorien und zeigt die Links in
## der ausgewählten Kategorie an. Wurde keine Kategorie angegeben, so
## wird "Top" angenommen.
public function startBrowse($catid = "")
{
$cats = $this->getCategories($catid);
$links = $this->getLinks($catid);
if (!empty($catid))
{
$currentid = $catid;
$currentname = $this->getCatNames($catid);
}
else
{
$currentid = "top";
$currentname = "top";
}
## Sind Kategorien auf der ausgewählten Ebene, so werden diese als
## Liste angezeigt.
if (!empty($cats))
{
while (list($key, $val) = each ($cats))
{
printf("<li><a href=\"%s?viewcat=%s\"><b>%s</b></a></li>",
$_SERVER['PHP_SELF'],
stripslashes($val["catid"]),
stripslashes($val["catname"]));
}
}
## Eine Linie trennt Kategorien und Links.
echo "<hr>\n";
## Falls Links vorhanden sind, so werden diese nun angezeigt.
if (is_array($links))
{
while (list($key, $val) = each ($links))
{
printf ("<li><a href=\"%s\"><b>%s</b></a> - %s</li>",
stripslashes($val["url"]),
stripslashes($val["linkname"]),
stripslashes($val["description"]));
}
}
## Erzeugen des Links, der zum Formular für eigene Vorschläge führt.
echo <<<LINK
<center>
<a href="{$_SERVER['PHP_SELF']}?add=$currentid&action=add">Einen neuen Link vorschlagen</a>
</center>
</div>
</body>
</html>
LINK;
return;
}
}
##### MAIN PROGRAM #####
## Im Hauptteil des Skripts werden die aktuellen Aktionen des Benutzers
## ausgewertet. Durch Aufruf der Methoden "startPage" und "startBrowse"
## wird die Ausgabe gesteuert.
$phpHoo = new phpHoo();
## Steuerung der normalen Blätterfunktion in den Kategorien, was über die
## get-Variable "viewcat" erfolgt.
if (isset($_GET['viewcat']))
{
$phpHoo->startPage($_GET['viewcat']);
$phpHoo->startBrowse($_GET['viewcat']);
exit;
}
## Auswertung der aktuellen Aktion und Speicherung in der Variablen
## "$action". Da der Benutzer sowohl Links anklicken als auch Formulare
## benutzen kann, wird hier "$_REQUEST" verwendet (eine Obermenge der
## Arrays "$_POST" und "$_GET"). Diese Variable ist die Grundlage für die
## Verzweigung mit "switch".
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : "";
switch ($action)
{
case "add":
## Erzeugen des Formulars, mit dem sich Einträge hinzufügen lassen.
## Im Formular wird das versteckte Feld "action" mit dem Wert "suggest"
## definiert, das die Steuerung nach dem Abschicken an den nächsten
## Block des Befehls "switch" übergibt.
$add = $_REQUEST['add'];
if(strtolower($add) == "top")
{
$add = 0;
}
$catname = stripslashes($phpHoo->getCatNames($add));
if (empty($catname)) { $catname = "Top"; }
echo <<<SUGGEST
<html>
<head>
<title>phpHoo - Link hinzufügen</title>
<link rel="stylesheet" type="text/css" href="phpHoo.css">
</head>
<body bgcolor="#EEEEEE" text="#000000" link="#0033FF" vlink="#660099">
<div class=text>
<center><h1>phpHoo</h1></center>
<h3>Einen Link zu <i>$catname</i> hinzufügen</h3>
<hr noshade>
<p><form action="{$_SERVER['PHP_SELF']}" method="post">
<input type="hidden" name="catid" value="$add">
<input type="hidden" name="action" value="suggest">
<table border="0" cellpadding="2" cellspacing="2">
<tr>
<td align="right"><b>URL:</b></td>
<td><input name="url" size="40" value="http://"></td>
</tr>
<tr>
<td align="right"><b>Titel:</b></td>
<td><input name="linkname" size="40"></td>
</tr>
<tr>
<td align="right"><b>Beschreibung:</b></td>
<td><textarea name="description" rows="3" cols="40"></textarea></td>
</tr>
<tr>
<td align="right"><b>Ihr Name:</b></td>
<td><input name="submitname" size="40"></td>
</tr>
<tr>
<td align="right"><b>Ihre E-Mail:</b></td>
<td><input name="submitemail" size="40"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" name="suggest" value="Vorschlag absenden">
<input type="reset" value=" Leeren "></td>
</tr>
</table>
<hr noshade>
</form></p>
</div>
</body>
</html>
SUGGEST;
break;
## Falls das Formular abgeschickt wurde, wird in Abhängigkeit von der Prüfung der
## Daten eine entsprechende Meldung erzeugt. Der Aufruf der Methode "suggest" führt
## zum Eintrag in die Datenbank, wenn keine Fehler auftraten oder Felder leer waren.
case "suggest":
$junk = "";
if (!$phpHoo->suggest())
{
$title = "Vorschlagsfehler";
$msg = "Vorschlag misslungen! Daten fehlten oder der Eintrag existiert bereits.\n";
}
else
{
$title = "Vorschlag empfangen";
$msg = "Der Vorschlag ist zur Prüfung eingegangen\n";
}
## Erneutes Anzeigen der Seite mit den Meldungstexten.
$phpHoo->startPage($junk, $title, $msg);
$phpHoo->startBrowse();
break;
## Aufruf, wenn eine Suchanfrage erfolgte.
case "search":
$hits = $phpHoo->search();
## Wurden keine Suchergebnisse gefunden, so wird eine entsprechende
## Meldung erzeugt.
if ((!$hits) or (empty($hits)))
{
$junk = "";
$title = "Suchergebnisse";
$msg = "Keine Seiten gefunden";
$phpHoo->startPage($junk, $title, $msg);
}
## Andernfalls Anzeige der Trefferanzahl und einer Trefferliste
else
{
$total = count($hits);
$title = "Suchergebnisse";
$msg = "Die Suche ergab [$total] Treffer";
$junk = "";
$phpHoo->startPage($junk, $title, $msg);
while (list($key, $hit) = each($hits))
{
if (!empty($hit))
{
$linkid = $hit["linkid"];
$linkname = stripslashes($hit["linkname"]);
$linkdesc = stripslashes($hit["description"]);
$linkurl = stripslashes($hit["url"]);
$catid = $hit["catid"];
$catname = stripslashes($phpHoo->getCatNames($catid));
echo <<<RESULTFORM
<dl>
<dt><a href="$linkurl" target="_new">$linkname</a></dt>
<dd>$linkdesc</dd>
<dd><b>Gefunden in:</b> <a href="{$_SERVER['PHP_SELF']}?viewcat=$catid">$catname</a></dd>
</dl>
RESULTFORM;
}
}
}
echo "<p>\n<hr>\n";
$phpHoo->startBrowse();
break;
## Falls ein unbekannter Befehl abgeschickt wurde, so wird einfach
## die Startseite angezeigt.
default:
$phpHoo->startPage();
$phpHoo->startBrowse();
break;
}
?>
|
Stylesheet
Erstellt werden sollte ebenfalls die Datei "phpHoo.css".