PHP/Anwendung und Praxis/Wechsellogik

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Informationen zum Autor

Name:
Thomas Schmieder
E-Mail:
Homepage:

Dieser Artikel beschreibt, wie man eine Wechsellogik für Banner, Farben, Bilder, Styles oder ähnliches serverseitig erzeugt.

Inhaltsverzeichnis

[Bearbeiten] Vorüberlegungen

Wer würde heutzutage in einer Suchmaschine schon nach „Wechsellogik für Bilder“ suchen? Da fällt einem doch eher „Banner Changer“ oder „Styleswitcher“ ein. Ein Changer macht daher nichts anderes, als bei jedem Request eine andere (zufällige) Untermenge von Namen aus einer größeren Kollektion zu liefern. Was der Programmierer dann im weiteren Verlauf damit anstellt, ist dem "Changer" egal.

Dies könnten

  • Farben
  • URLs
  • Sprüche des Tages (die Nummer oder der Dateiname)
  • Benutzernamen (wer wäscht heute ab?)
  • ...

sein

[Bearbeiten] Aufgabenstellung

Die mathematische Aufgabenstellung lautet, dass eine zufällige Auswahl aus einer Auswahlmenge geliefert werden soll. Damit eine Auswahl nicht rein zufällig mehrfach hintereinander gezogen werden kann, soll die Methode „Ziehen ohne Zurücklegen“ aus dem Urnenmodell verwendet werden.[1]

Die ausgegebenen Werte sollen aus der Auswahlmenge verschwinden und erst dann wieder als mögliche Ausgabewerte zur Verfügung stehen, wenn alle Werte der Ausgangsmenge verbraucht sind.

[Bearbeiten] Berührte Themen

Die kleine Funktion berührt diverse Teilbereiche der PHP-Programmierung, warum ich sie hier vorstellen möchte.

  • Zufallsauswahl mit Arrays
  • persistente Zwischenspeicherung zwischen den Requests
  • Gültigkeitsbereich für jeden Besucher, oder benutzerbezogen?
  • Dateisperren richtig einsetzen
  • Passende Auswahl der Zufallsfunktion: rand(), mt_rand(), array_rand(), shuffle()
  • Welches Dateiformat zum Speichern: Sessiondatei, serialized Array, Ini-Format, Datenbank?
  • Möglichkeiten der Fehlerbehandlung in PHP

Es geht hier also weniger um eine komplizierte Programmlogik, als vielmehr um die Erläuterungen, warum genau diese Programmlogik in dieser Form entstanden ist. Selbstverständlich haben die Lösungen auch einen häufig nachgefragten Nutzwert und können als Basis für eigene Weiterentwicklungen herhalten. Im Abschnitt Erläuterungen findet man daher eine begleitende Dokumentation, die den meisten Programmen auf dem Markt vollkommen fehlt.

[Bearbeiten] Anwendungsbeispiel

[Bearbeiten] prozedurale Lösung

Als Einstieg hier eine prozedurale Lösung, also klassische "Top-Down-Programmierung"

Achtung!

Dieses Script funktioniert erst für PHP-Version >= 5.3.0!


Beispiel: Script (PHP >=5.3.0)
<?php   ### changer.php ### Wechselnde Ausgabe ### utf-8 ### ÄÖÜäöü

### only for testing
header('Content-Type: text/html; Charset=utf-8');
$filename = 'changer.dat';	## Dateiname für die Datendatei

 
#------------------------------------------------------------------------------
## settings
#------------------------------------------------------------------------------
    $MYERRORMSG = NULL;			 ## Globale Variable zur Fehlerauswertung
    ini_set('track_errors',1);           ## Für die Fehlerbehandlung notwendig
    $abort = ignore_user_abort(true);    ## Damit das File fertig geschrieben wird
                                         ##   wenn der User vorher schlapp macht.	

#------------------------------------------------------------------------------
function get_error_msg($error_txt)
{
    ## ToDo: 
    ## für alle verfügbaren Texte aus $php_errormsg eindeutige 
    ## Fehlernummern festlegen

    return $error_txt;
}
 
#------------------------------------------------------------------------------
function changer($filename, $number=1)
{
    GLOBAL $MYERRORMSG;
 
    ## Datei öffnen zum Lesen und Schreiben, 
    ## wenn _nicht_ vorhanden, vorher anlegen
    $fp = @fopen($filename, 'cb+');
    if (!$fp)
    {
	$MYERRORMSG = get_error_msg($php_errormsg);
	return false;
    }
 
    if (!@flock($fp, LOCK_EX))
    {
	$MYERRORMSG = get_error_msg($php_errormsg);
	return false;
    }
 
    $_fstat = fstat($fp);
    $cont = fread($fp, $_fstat['size']);
 
    $_data = @parse_ini_string($cont, true);
    if ($_data === false)
    {	
        $MYERRORMSG = "1000 - Fehler beim Parsen der Datenquelle $filename: $php_errormsg";
	return false;
    }
 
    if (!isset($_data['active'], $_data['reserve']))
    {
	$MYERRORMSG = '1010 - allg. Fehler in der Datenquelle';
	return false;
    }
 
    if (count($_data['active']) === 0  && count($_data['reserve']) === 0)
    {
	$MYERRORMSG = '1011 - Datenquelle ist leer';
	return false;
    }
 
    if (count($_data['active']) + count($_data['reserve']) < $number)
    {
	$MYERRORMSG = '1012 - Datenquelle enthält zuwenig Daten';
	return false;
    }
 
    $_selection = array();
    $shuffle_flag = false;
 
    for ($i = 0; $i < $number; $i++)
    {
	if (count($_data['active']) === 0)
	{
            $_data['active'] = $_data['reserve'];
 
	    ## reserve-Array wieder leeren
     	    $_data['reserve'] = array();
            $shuffle_flag = true;
	}
 
	$shift = array_shift($_data['active']);
	$_selection[] = $shift;
	$_data['reserve'][] = $shift;
    }
 
    ## erst hier mischen, weil sonst in $_selection Einträge mehrmals auftreten könnten
    if ($shuffle_flag)
    {
	shuffle($_data['active']);	
	shuffle($_data['reserve']);	
    }	
 
    ftruncate ($fp, 0);	
    fseek($fp, 0, SEEK_SET);
    fwrite ($fp, ';### ' . basename($filename) . ' ### ' . date('Y-m-d H:i:s') . ' ### utf-8 ### ÄÖÜäöü' . PHP_EOL);
 
    fwrite ($fp, '[active]' . PHP_EOL);
    foreach($_data['active'] as $key => $value)
    {
        fwrite ($fp, "$key = $value" . PHP_EOL);
    }
 
    fwrite ($fp, PHP_EOL . '[reserve]' . PHP_EOL);
    foreach($_data['reserve'] as $key => $value)
    {
        fwrite ($fp, "$key = $value" . PHP_EOL);
    }
 
    fclose($fp);
 
    return $_selection;
}
 
#==============================================================================
# php unit test
#==============================================================================

echo "<pre>\r\n";
echo htmlspecialchars(print_r(changer($filename,4),1)) . "\r\n";
echo $MYERRORMSG . "\r\n";
echo "</pre>\r\n";
 
?>

[Bearbeiten] OOP-Lösung

ToDo (weitere ToDos)

Lösung als Klasse

[Bearbeiten] class changer

Beispiel: class changer
<php
 
class changer()
{
 
}
 
#==========================================================================================
# php unit test
#==========================================================================================

$changer = new changer();
echo $changer->get_next();
 
 
 
?>

[Bearbeiten] Datendatei

Die Datendatei ist im PHP-Ini-Format aufgebaut. Es wurden hier zwei Sectionen angelegt. Die Datendatei muss vor Programmstart vorhanden sein. Weitere Sektionen werden von der Funktion nicht unterstützt und verschwinden im Betrieb. Wer dies nicht möchte, kann für das Speichern der Daten die Funktion aus dem Wiki-Artikel INI-Dateien aus einem Array erstellen benutzen.


Beispiel: Datendatei
;### changer.dat ### 2014-05-09 16:21:00 ### utf-8 ### ÄÖÜäöü
[active]
0 = 'schwarz'
1 = 'braun'
2 = 'rot'
3 = 'orange'
4 = 'gelb'
5 = 'grün'
6 = 'blau'
7 = 'violett'
8 = 'grau'
9 = 'weiß'
10 = 'gold'
11 = 'silber'
 
[reserve]
 
;### Am Anfang keine Einträge

[Bearbeiten] Erläuterungen

Die folgenden Erläuterungen sollen helfen, das kleine Script wirklich zu verstehen. Sie dienen aber auch als Mahnung an viele Programmierer, die sich "zu gut dafür" sind, Erläuterungen zu ihren Lösungen zu verfassen!

[Bearbeiten] Warum eine "INI-Datei" für die Datenspeicherung?

Wir haben uns bei der Datendatei für das INI-Format von PHP entschieden, weil die Datei so mit einem einfachen Editor erstellt werden kann und leicht lesbar ist für Menschen.

Die [reserve]-Section ist üblicherweise am Anfang leer. In der [active]-Section stehen die zur Auswahl verfügbaren Optionen. Die Indexzuordnung geht bei Benutzung verloren. Es ist auch unerheblich, ob man mit a,b,c, oder 1,2,3 oder 'Hans','Hilde','Horst' indiziert. Es darf innerhalb einer Section ein Indexwert nur nicht doppelt vorkommen. Dann würde der zweite Datenwert (auf den der Index verweist) das erste Vorkommen überschreiben und der erste damit verloren gehen.

Mit jedem Aufruf wandern aus der [active]-Section $number Einträge in die [reserve]-Section. Diese werden von der Funktion changer() in einem Array auch als Rückgabewerte geliefert.

Wenn das Funktionsergebnis ===false ist, steht in $MYERRORMSG die zugehörige Fehlermeldung.

[Bearbeiten] Warum benutzen wir nicht parse_ini_file()?

PHP bietet zur Analyse von INI-Dateien seit Version 5.3.0 zwei Funktionen an:

Durch Einführung der Funktion parse_ini_string() ist es möglich geworden, Ini-Files auch POSIX-konform zu benutzen, also mit den vorgeschriebenen Dateisperren. Dateisperren sind im konkurrierenden Umgang mit Dateien (Multi-User-Anwendung) grundsätzlich notwendig, um die Datenintegrität zu bewahren.

Das Öffnen, Sperren, Lesen, und Schreiben von Ini-Files können wir dank parse_ini_string() nun mit den dafür geeigneten File-Handle-Funktionen vornehmen. Mit parse_ini_file() wäre es nicht möglich, die Datei ordnungsgemäß zu sperren.

[Bearbeiten] Warum benutzen wir nicht file_get_contents() und file_put_contents()?

Um einen Lese-Schreib-Zyklus auf eine Datei ordnungsgemäß abzusichern, muss folgender Ablauf gewahrt werden:

  • Datei öffnen
  • Datei sperren
  • Datei lesen
  • gelesenen Inhalt verändern
  • Datenzeiger auf passende Position (hier auf den Anfang der Datei) zurücksetzen
  • veränderte Daten zurückschreiben
  • Wenn die Länge der Datei verkleinert wurde, Datei passend kürzen
  • Datei schließen inclusive:
    • Buffers flushen
    • Sperre aufheben
    • Schließen

Mit den namensbasierten Filefunktionen ist ein solches atomares oder atomistisches Konstrukt nicht möglich. Es würde zwischen dem Lese- und dem Schreibvorgang eine ungeschützte Lücke entstehen.[2]

Ein konsequenter Schutz ist in den üblichen Filesystemen nur mit den File-Handle-Funktionen möglich.

[Bearbeiten] Warum führen wir eine globale Variable $MYERRORMSG" ein?

Die konsequente PHP-Fehlerbehandlung in der prozeduralen PHP-Welt wurde einfach vergessen. Es werden keine eindeutigen Fehlernummern geliefert, sondern Fehlertexte. Diese muss man dann erst wieder "zurückübersetzen".

Um dies zu begünstigen, bedienen wir uns der Systemvariablen $php_errormsg, die uns bei passender Einstellung ini_set('track_errors', 1) zur Verfügung steht. Nur, weil wir die Fehler nun selber auswerten, erlauben wir uns den @-Schalter vor der zu überwachenden Funktion einzusetzen. Dieser unterdrückt den Export von Fehlermeldungen auf die Standardausgabe oder in die Logdatei (je nach Einstellungen in der php.ini).

Leider steht $php_errormsg nur im lokalen Scope (also innerhalb der Funktion, in der sie belegt wird) zur Verfügung. Die Variable kann man auch nicht mit GLOBAL in den jeweiligen Kontext oder das globale Scope exportieren. Ihr Bezeichner ist dann zwar dort bekannt, die Variable ist aber leer und damit für uns nutzlos. Damit haben sich die Entwickler von PHP gleich zwei schwere Konzeptfehler auf einmal geleistet!

Dies ist daher eine der ganz wenigen Stellen, an denen das Umkopieren eine Variablen in PHP sinnvoll ist.

Wenn unsere Funktion nun false als Ergebnis liefert, steht in $MYERRORMSG die letzte Fehlermeldung.

[Bearbeiten] Wie wir Fehler mit error_get_last() anstelle von $php_errormsg behandeln können

PHP bietet für die Fehlerbehandlung seit der Version 5.2.0 auch die Funktion error_get_last() an, die ohne jegliche Konfigurationseinstellung oder besondere Randbedingungen ein Array mit Fehlerinformationen liefert. Das wird bei jeder fehlerbewehrten Funktion automatisch im Hintergrund gefüllt. Für eigene Fehler, z.B. aus einzuhaltenden Geschäftsregeln unserer Funktion, müssen wir es selber füttern.

Array
(
    [type] => 2
    [message] => fopen(bekannt_ro.txt): failed to open stream: Permission denied
    [file] => M:\USER\TOM\WebProgTests\Xampp\error_msg\error.php
    [line] => 21
)

Das Array enthält alle Daten, die sonst auch als Meldung angezeigt werden. Die Funktion ist ohne Konfigurationsänderung benutzbar, steht im globalen Scope (also auch außerhalb der zu überwachenden Funktion) zur Verfügung, funktioniert mit und ohne @.

Mit der Funktion @trigger_error($message, E_USER_NOTICE) können wir dem Array auch eine [message] zuweisen, ohne dass diese sofort zur Anzeige gelangt.

Leider liefert uns auch error_get_last() nur eine textuelle Fehlermeldung, die wir für die Programmfluss-Steuerung erst noch zum Fehlercode umdeuten müssen.

[Bearbeiten] Warum shuffle()?

PHP bietet mehrere Zufalls- und Mischfunktionen an:

Da man auch mehrere geordnet entnommene Werte aus einer durchmischten FiFo-Liste immer noch als Zufallswerte bezeichnen kann, müssen wir pro Listenaufbau nur einmal mischen. Dies ist am einfachsten mit shuffle() zu erreichen. Da das gemischte Array zwischengespeichert wird, ist der Aufwand von shuffle()gegenüber einer separaten Zufallscode-Bestimmung mit rand() oder mt_rand() pro Wertentnahme geringer. Auch array_rand() würde vermutlich mehr Aufwand erfordern, da dann zusätzlich ein unset() pro entnommenem Element notwendig wäre. Außerdem müsste array_rand() bei jedem Request benutzt werden, da es mit Indices arbeitet und die Indexreihe nach der Entnahme jedes Mal Lücken enthält.[3]


Die Kombination aus einem "gelegentlichen" shuffle() und einem array_shift() pro Entnahme erscheint daher am sparsamsten, insbesondere bei langen Listen und kleinen Entnahmemengen.

[Bearbeiten] Ausprobieren

[Bearbeiten] Quellen

  1. Wikipedia: Urnenmodell
  2. Wikipedia: Time-of-Check-to-Time-of-Use-Problem
  3. Selfhtml-Forum: Der Zufall mt_rand ist nicht zufällig genug vom 02.08.2016
Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Index
Mitmachen
Werkzeuge
Spenden
SELFHTML