PHP/Tutorials/INI-Datei aus einem Array erstellen

Aus SELFHTML-Wiki
< PHP‎ | Tutorials
Wechseln zu: Navigation, Suche
PHP bietet den Dateityp "INI-Datei" an und stellt dafür zwei Lesefunktionen zur Verfügung, mit deren Hilfe man die Datei einlesen kann.[1] Leider gibt es noch keine fertige Funktion für das Schreiben (Erstellen) einer INI-Datei. Daher stelle ich hier eine solche Funktion vor.

Die Funktion erstellt eine INI-Datei aus einem Array. Das Array darf mehrere Ebenen haben. PHP kann allerdings nur mit maximal 3 Ebenen in den Lesefunktionen umgehen. Daher ist das Funktionsargument "depth" auf den Wert 3 voreingestellt. Die Funktion ini_write() kann im Prinzip Arrays mit beliebiger Tiefe verarbeiten. Die maximale Tiefe ist nur von der maximal zulässigen Rekursionstiefe und dem verfügbaren Hauptspeicher und der Größe des Stacks abhängig. PHP empfiehlt, eine Rekursionstiefe von 100 nicht zu überschreiten.

Format der Ini-Datei

  • Die INI-Datei kann Sektionen enthalten. Diese werden durch [section_name] eingeleitet.
  • Kommentare beginnen mit einem Semikolon.
  • Pro Zeile steht ein Name-Value-Pärchen
  • Werte werden üblicherweise in einfachen oder doppelten Anführungszeichen eingeschlossen.
  • Das als Textbegrenzer gewählte Anführungszeichen geht beim Einlesen verloren, wenn es im Text ebenfalls enthalten ist.
  • Arrays dürfen maximal eine Tiefe von zwei Ebenen haben, hinzu kommt dann ggf. noch die Ebene der Sektion, sodass sich eine maximale Gesamttiefe von drei Ebenen ergibt.
  • In den Bezeichnern des Keystrings sind die Zeichen ?{}|&~![]()^"= nicht gestattet.
  • Array-Elementbezeichner werden – anders als im Programm – ohne Quotierungen notiert


Beispiel-Datei
;### ini_file.dat ### 2014-05-20 13:12:11 ### utf-8 ### ÄÖÜäöü

param1 = 'eins'
param2 = 'zwei'
param3 = 'drei'

[meta]
calls = '9'

[active]
0 = 'braun'
1 = 'rot'
2 = 'grau'
3 = 'blau'
4 = 'violett'
5 = 'grün'
6 = 'orange'
7 = 'gold'

[reserve]
0 = 'weiß'
1 = 'silber'
2 = 'schwarz'
3 = 'gelb'

[tom]
alter = '56'
wohnung[plz] = '34444'
wohnung[ort] = 'Sankt Andreasberg'
wohnung[strasse] = 'Mühlenstraße'
wohnung[hausnummer] = '19'
tel[0] = '05582/999378'
tel[1] = '05582/999xxx'

Funktionen

Lesefunktion

ini_read()
function ini_read($fp)	
{	
    if (!is_resource($fp)) return false;
    if (false === ($_fstat = @fstat($fp))) return false; 	
    if (false === ($cont = fread($fp, $_fstat['size']))) return false;
    if (false === ($_data = @parse_ini_string($cont, true))) return false;

    return $_data;
}

Wenn ausschließlich gelesen werden soll, trotzdem nicht vergessen, den mit fopen() beschafften filepointer mit flock() mit LOCK_SH gegen Schreiben durch Andere zu sperren.

Schreibfunktion

$fp Ressourcenkennung der geöffneten Datei
$_data zu verarbeitende Daten (Array)
$filename Dateiname, ausschließlich zur textlichen Verwendung innerhalb der Datei (Kopfzeile)
$maxdepth Maximale Verarbeitungstiefe für das Datenarray.


ini_write()
function ini_write($fp, $_data, $filename, $maxdepth=3)
{

    #--private Funktion -------------------------------------------------------------------------
    $writeparams = function ($_values, $arraykey, $depth) use ($fp, $maxdepth, &$writeparams)
    {
	foreach ($_values as $key => $param)
	{
            if ($depth >= 1)
	    {
		$arraykeytxt = $arraykey . "[$key]";
	    }	
	    else
	    {   
		$arraykeytxt = $key;
	    }

	    if (is_array($param))
	    {
		$depth++;
		if ($depth < $maxdepth)
		{
	            if (false === $writeparams ($param, $arraykeytxt, $depth)) return false;
		}	
	    }
	    else
	    {
		if (false === @fwrite ($fp, "$arraykeytxt = '$param'" . PHP_EOL)) return false;	
	    }
	}

	return true;
    };
    #------------------------------------------------------------------------------------------

    if ( 0 !== @fseek($fp, 0, SEEK_SET)) return false;
    if (false === @fwrite ($fp, ';### ' . basename($filename) . ' ### ' . 
        date('Y-m-d H:i:s') . ' ### utf-8 ### ÄÖÜäöü' . PHP_EOL . PHP_EOL)) return false;

    $depth = 0;
    $arraykey = '';
	
    foreach ($_data as $section => $_value)
    {
	if (is_array($_value))
	{
            if (false === @fwrite ($fp, PHP_EOL . "[$section]" . PHP_EOL)) return false;
			
            if ($depth < $maxdepth) 
            {
	    	$writeparams ($_value, $section, $depth); 
	    }
	}	
	else
        {
            if (false === @fwrite ($fp, "$section = '$_value'" . PHP_EOL)) return false;	
        }		
    }
	
    if (false === ($len = @ftell($fp))) return false;
    if (false === @ftruncate($fp, $len)) return false;
	
    return true;
}

Funktionen testen

php unit test
<?php  
### parse_ini_file.php 
### Ini-File auslesen und zurückschreiben 
### utf-8 ### ÄÖÜäöü
    ### only for testing
    header('Content-Type: text/html; Charset=utf-8');
    $source = 'ini_file.dat';	## Dateiname für die Datendatei


    echo "<pre>\r\n";

    ### Datei öffnen ###
    if (false === ($fp = @fopen($source, 'rb+')))
    {
        echo htmlspecialchars(print_r(error_get_last(),1)) . "\r\n";
        exit;
    }

    ### Datei zum Lesen und Schreiben sperren ###
    if (!flock($fp, LOCK_EX))
    {
        echo htmlspecialchars(print_r(error_get_last(),1)) . "\r\n";
        exit;
    }


    ### Datei einlesen
    if (false === ($_data = ini_read($fp))) 
    {
        echo htmlspecialchars(print_r(error_get_last(),1)) . "\r\n";
    }
    else 
    {
	echo htmlspecialchars(print_r($_data,1)) . "\r\n";

        ### hier können die Daten in $_data geändert werden
        $_data['meta']['calls'] ++ ;

        ### Datei zurückschreiben. 
        if (false === ini_write($fp, $_data, $target, 3)) 
            echo htmlspecialchars(print_r(error_get_last(),1)) . "\r\n";
    }	

    echo "</pre>\r\n";

    fclose($fp);
?>

Erläuterungen

Rekursion

Das Ini-Format ist etwas "eigenwillig" aufgebaut, dafür aber sehr leicht von Menschen mit einem simplen Texteditor lesbar und auch schreibbar. Durch die ggf. vorangestellten [Sektionsbezeichner] müssen wir beim Erstellen aus dem Array zunächst prüfen, ob das Array-Element selber wieder eine Liste (ein Array) beinhaltet. Nur dann wird ein [Sektionsbezeichner] geschrieben.

Außerdem müssen wir prüfen, in welcher Ebene der Ausgabe wir uns befinden, da der Name immer mit einem ungeklammerten Bezeichner beginnt. Erst ab der nächsten Ebene wird, wie bei PHP-Arrays üblich, der Elementbezeichner in eckigen Klammern angegeben.

Um Parameternamen mit beliebiger Tiefe verarbeiten zu können, werden Subarrays des Datenarrays $_data rekursiv verarbeitet. Hierzu benutzen wir die anonyme Funktion, die der Variable $writeparams zugewiesen wurde. Wenn die vorgegebene Schachtelungstiefe $maxdepth überschritten wird, wird die Ausgabe der Wertzuweisung unterdrückt. Die für die Lesefunktionen parse_ini_file() und parse_ini_string() zulässige Schachtelungstiefe beträgt Drei.

Anonyme Funktion

Da die für die Rekursion benötigte Funktion nur speziell für die Hauptfunktion geeignet ist und für andere Zwecke nicht brauchbar wäre, habe ich eine anonyme Funktion dafür gewählt. Außerdem wollte ich auch einmal damit herumspielen :-)

Die Funktion wird mit dem Funktionskopf

$writeparams = function ($_values, $arraykey, $depth) use ($fp, $maxdepth, &$writeparams)

deklariert. Die Funktionsargumente stehen für

  • $_values der zu verarbeitende Teil des Datenarrays
  • $arraykey der im Verarbeiungszweig bisher aufgelaufene Bezeichnertext
  • $depth die aktuelle Verarbeitungstiefe

Durch use ($fp, $maxdepth, &$writeparams) werden der Funktion die benötigten Kontext-Werte bekannt gemacht.

  • $fp der Dateizeiger der geöffneten Datendatei
  • $maxdepth die vorgegebene maximale Schachtelungstiefe
  • &$writeparams die Referenz auf die Funktion selbst. Ohne diese Angabe würde die Funktion sich in der nächsten Rekursionsebene selber nicht mehr kennen. Bitte auf das & für die Referenz achten.

Im Funktionskörper { ... }; wird die Funktion, wie üblich definiert. Einziger Unterschied zu "normalen" Funktionen ist, dass der Funktionsblock hier mit einem Semikolon abgeschlossen werden muss, da die gesamte Funktionsdeklaration und -definition quasi in einer einzigen Programmanweisung stattfinden.

Lesen, Ändern, Schreiben

Bei beiden Funktionen ini_read() und ini_write() arbeiten wir ganz bewusst mit einer Datei-Ressourcekennung ($fp) und nicht mit einem Dateinamen. Dies hat den Sinn, dass die Ressourcekennung von einer Funktion an die andere weitergegeben werden kann und damit der gesamte Prozess auch atomar gekapselt werden kann:

  • Datei öffnen
  • Datei exklusiv sperren
  • Datei lesen
  • Daten ändern
  • Dateizeiger passend positionieren (hier: zurücksetzen auf Anfang)
  • Daten schreiben
  • Datei passend einkürzen (eigentlich nur notwendig, wenn sie kürzer geworden ist)
  • Datei schließen
    • beinhaltet Datenpuffer flushen,
    • Datei schließen
    • Datei entsperren

Das Einkürzen wird bewusst erst am Ende des Vorganges durchgeführt, da dadurch bei einem Fehler (Stromausfall) ggf. die Datei noch rettbar bleibt.

ToDo (weitere ToDos)

  • Verbotene Zeichen im Name-Parameter verhindern
  • passende Quotierung auswählen (je nachdem, welche Quotes im Wert-Parameter schon vorhanden sind)
  • Sicherstellen, dass ein CRLF oder LF im Wert-Parameter keinen Fehler verursacht
  1. Wikipedia: Initialisierungsdatei