Stosując pliki jako magazyn informacji przetwarzanych przez skrypt PHP należy przestrzegać pewnych zasad. Dzięki temu unikniemy błędów, zaś same skrypty będą bardziej wydajne. Wstępna klasyfikacja plików oraz skryptów pod względem metod dostępu i współbieżności wykonania pomaga uniknąć ewentualnych problemów.
O dostępie do pliku mówimy wówczas, gdy skrypt wywołuje którąkolwiek z funkcji opisanych w dziale Function reference → Filesystem function. Głównie chodzi o funkcje otwarcia, zamknięcia, odczytu, zapisu, zablokowania, modyfikacji położenia wskaźnika oraz uzyskania informacji o pliku. Wywoływanie tych funkcji, w zależności od okoliczności, podlega większym lub mniejszym ograniczeniom. Wpływ na to mają dwa czynniki: tryb dostępu do pliku oraz współbieżność wykonania skryptów.
Powyższe kryteria rozważamy w stosunku do każdego pliku. Mamy wówczas, w przypadku kryterium tryb dostępu, do czynienia z następującymi przypadkami:
Drugie kryterium, współbieżność wykonania operacji plikowych, wprowadza podział na skrypty:
Przykładowym skryptem dostępnym on-line jest ponownie licznik wizyt na stronie. Strona jest odwiedzana przez wielu gości, każda wizyta powoduje zapisanie informacji do pliku. Natomiast skryptami administracyjnymi są między innymi skrypty konwertujące dokumenty tworzące witrynę (np. zmiana kodowania polskich znaków lub zmiana krótkich znaczników php <? w znaczniki pełne <?php).
W stosunku do skryptów administracyjnych przyjmiemy dwa założenia:
Tabela 1 przedstawia sumaryczne zestawienie sytuacji, jakie mogą mieć miejsce przy uwzględnieniu powyższych dwóch klasyfikacji.
| Odczyt pliku czytanego | Odczyt/zapis | Odczyt pliku zapisywanego | |
|---|---|---|---|
| Administracyjne | Niekonieczna blokada. Odczyt: file(), file_get_contents(), file_n(), dowolne inne |
Niekonieczna blokada. Odczyt: file(), file_get_contents(), file_n(), dowolne inne Zapis: pojedyncze wywołanie file_put_contents() |
Zakładamy brak współbieżności wykonania. Taka sytuacja nigdy nie ma miejsca. |
| On-line | Niekonieczna blokada. Odczyt: file(), file_get_contents(), file_n(), dowolne inne. Żaden skrypt nie zapisuje danych do pliku. |
Konieczna blokada LOCK_EX Zapis: pojedyncze wywołanie fwrite() Odczyt: pojedyncze wywołanie fread() |
Zalecana blokada LOCK_SH Odczyt: pojedyncze wywołanie fread() |
Tabela 1. Charakterystyka dostępu do pliku
Zwracając uwagę na powyższe podziały możemy sformułować pierwszą regułę, która ma na celu zapewnienie poprawności wykonywania operacji plikowych.
Analizując tabelę 1 zauważymy, że w istocie mamy do czynienia z czterema istotnie różnymi przypadkami:
Pierwszym przypadkiem, jaki rozważymy praktycznie, jest odczyt danych z pliku otwieranego wyłącznie do odczytu. Jeśli plik jest wyłącznie odczytywany przez skrypty i żaden ze skryptów nigdy nie otwiera go w trybie do zapisu, wówczas — bez względu na to czy rozważamy skrypt administracyjny czy on-line — nie musimy stosować zabezpieczeń dostępu do pliku.
W takiej sytuacji możemy korzystać z dowolnej spośród funkcji odczytujących dane z plików: readfile(), file_get_contents(), file(), parse_ini_file(), fread(), fgets(), fgetc(), fgetss(), fscanf() oraz fgetcsv(). Zwróćmy jednak uwagę na fakt, że jeśli przeczytany ma być cały plik do zmiennej będącej napisem, to zdecydowanie najlepiej użyć funkcji file_get_contents(). Natomiast w przypadku, gdy wymagany jest podział na wiersze stosujemy funkcję file(). Wreszcie w przypadku, gdy potrzebnych jest kilka początkowych wierszy pliku, należy przygotować własną funkcję file_n(), która odczyta wymaganą ilość linii i zwróci ją w odpowiedniej postaci. W innych, bardziej specyficznych przypadkach, stosujemy własne implementacje odczytu danych.
Funkcje file_get_contents() oraz file_put_contents() pojawiły się w PHP w wersjach 4.3.0 oraz 5 CVS odpowiednio. W przypadku, gdy funkcje te nie są dostępne, należy je samodzielnie przygotować lub wykorzystać bibliotekę PEAR (pakiet PHP_Compat).
Po zapisaniu funkcji file_get_contents() oraz file_put_contents() jak również odpowiednich stałych do pliku file_xxx_contents.inc.php, w skrypcie umieszczamy instrukcję:
require_once 'file_xxx_contents.inc.php';
Od tej pory obie wymienione funkcje będą dostępne, zaś ze względu na instrukcję:
if (!function_exists('file_get_contents')) {
...
}
skrypt będzie działał poprawnie bez względu na wersję PHP. Jeśli podana funkcja jest już dostępna (czyli funkcja function_exists() zwraca wynik true) to ponowna definicja nie jest dołączana, w przeciwnym przypadku — definiujemy brakującą funkcję.
if (!function_exists('file_get_contents')) {
function file_get_contents(
$filename,
$incpath = false,
$resource_context = null
)
{
if (false === $fh = fopen($filename, 'rb', $incpath)) {
trigger_error('failed to open ...', E_USER_WARNING);
return false;
}
clearstatcache();
if ($fsize = @filesize($filename)) {
$data = fread($fh, $fsize);
} else {
$data = '';
while (!feof($fh)) {
$data .= fread($fh, 8192);
}
}
fclose($fh);
return $data;
}
}
Listing 1. Funkcja file_get_contents() z biblioteki PEAR.
Pierwsza seria przykładów ilustruje typowe zadania wymagające odczytania pliku. Przekształcamy wysyłany plik, zamieniając znaki złamania wiersza w znaczniki HTML:
echo nl2br(file_get_contents('piesn.txt'));
Kompresujemy dane zawarte w pliku, po czym wysyłamy je do klienta dołączając odpowiedni nagłówek:
$tekst = file_get_contents('piesn.txt');
$spak = gzencode($tekst, 9);
header('Content-Type: text/plain');
header('Content-Encoding: gzip');
echo $spak;
Niemal identycznie postępujemy, gdy chcemy wysłać do klienta spakowaną stronę WWW:
$tekst = file_get_contents('strona.html');
$spak = gzencode($tekst, 9);
header('Content-Type: text/html');
header('Content-Encoding: gzip');
echo $spak;
W przykładzie czwartym, wysyłamy do klienta plik slonecznik.jpg:
$obraz = file_get_contents('slonecznik.jpg');
header("Content-type: image/jpeg");
header('Content-Disposition: inline; filename=p.jpg');
echo $obraz;
Konwersja pliku JPEG do formatu PNG wymaga użycia funkcji imagecreatefromstring() oraz imagepng() z biblioteki GD:
$gdobj = imagecreatefromstring($obraz);
header("Content-type: image/png");
imagepng($gdobj);
Jeśli zawartość pliku ma być poddana przetwarzaniu wiersz, po wierszu, wówczas stosujemy funkcję file():
foreach (file('rivers.txt') as $line) {
$el = explode('*', trim($line));
echo str_pad($el[1], 10, '.') .
str_pad('(ang. ' . $el[0] . ')', 15, '.');
}
Ostatni z serii przykładów demonstrujących odczyt informacji z pliku pokazuje, w jaki sposób postępować, gdy potrzebne są tylko początkowe linie pliku. W takiej sytuacji, przygotowujemy funkcję file_n(), której zadaniem jest odczytanie podanej liczby linii z pliku. Po dołączeniu do skryptu pliku file-n.inc.php odczytujemy z pliku dwie pierwsze linie:
require_once 'file-n.inc.php';
$plk = file_n('wierszyk-1.txt', 2);
echo $plk[0];
echo $plk[1];
Kod funkcji file_n() został przedstawiony na listingu 2.
function file_n($AFileName, $AIleLinii = 1, $ABufor = 4096)
{
$wynik = array();
$hd = fopen($AFileName, 'rb');
if (!$hd) {
return false;
}
for ($i = 0; $i < $AIleLinii; $i++) {
$wynik[] = fgets($hd, $ABufor);
}
fclose($hd);
return $wynik;
}
Listing 2. Funkcja file_n() odczytująca z pliku zadaną liczbę linii.
Dodajmy, że oczywiście możemy wywoływać dowolne inne funkcje dostępu do pliku, pod warunkiem, że żaden ze skryptów nie otwiera pliku w trybie do zapisu.
W przypadku zapisywania danych do plików bardzo istotną rolę odgrywa podział na skrypty administracyjne i skrypty on-line. W przypadku skryptów administracyjnych nie musimy stosować blokowania dostępu do pliku. Natomiast skrypty dostępne on-line muszą uwzględniać wszystkie możliwe zabezpieczenia, na czele z blokowaniem dostępu. Pamiętajmy, że współbieżne wykonanie kilku skryptów administracyjnych — ze względu na brak odpowiednich zabezpieczeń — może doprowadzić do zniszczenia danych zawartych w plikach.
Sama technika wykonywania operacji zapisu danych jest stosunkowo prosta. W skrypcie on-line dane zapisujemy pojedynczym wywołaniem funkcji fwrite(). Natomiast w skrypcie administracyjnym stosujemy pojedyncze wywołanie funkcji file_put_contents().
Przejdźmy do analizy przykładowych skryptów administracyjnych zapisujących dane do pliku.
W pierwszym przykładzie zawartość pliku piesn.txt podlega kompresji:
$tekst = file_get_contents('piesn.txt');
$spak = gzencode($tekst, 9);
file_put_contents('sp.txt.gz', $spak);
Zawartość pliku jest najpierw odczytywana z pliku piesn.txt (funkcja file_get_contents()), a następnie zapisywana do pliku o nazwie sp.txt.gz (funkcja file_put_contents()).
Przetwarzanie przebiega podobnie w przypadku naprawy pliku HTML funkcjami z biblioteki tidy:
$plk = file_get_contents('strona.html');
$ok = tidy_repair_string($plk);
file_put_contents('strona-ok.html', $ok);
konwersji znaków funkcjami z rodziny Multibyte String Functions:
$tmp = file_get_contents('piesn.txt');
$tmp = mb_convert_case($tmp, MB_CASE_TITLE, "ISO-8859-2");
file_put_contents('piesn-new.txt', $tmp);
czy zmiany kodowania polskich znaków w pliku:
$org = file_get_contents('polski-alfabet-win.txt');
$tmp = pl_win2iso($org);
file_put_contents('x-win2iso.txt', $tmp);
Konwersja pliku JPEG do formatu PNG w oparciu o bibliotekę GD wymaga wykorzystania bufora danych wyjściowych:
$strSrc = file_get_contents('slonecznik.jpg');
// obraz źródłowy jest gotowy
$gdobj = imagecreatefromstring($strSrc);
ob_clean();
ob_start();
imagepng($gdobj);
$strDest = ob_get_contents();
ob_clean();
// obraz wynikowy jest gotowy
file_put_contents('plk.png', $strDest);
Natomiast zmiana formatu pliku tekstowego — z racji na potrzebę przetworzenia linia po linii — stosuje funkcje file() oraz pętlę foreach:
$plik = file('dane.txt');
$linie = array();
foreach ($plik as $wiersz) {
$elementy = explode(':', trim($wiersz));
$nowy = '';
foreach ($elementy as $el) {
$nowy .= '"' . trim($el) . '",';
}
//usuwamy ostatni przecinek
$nowy = rtrim($nowy, ',');
$linie[] = $nowy;
}
$nowyFormat = implode("\r\n", $linie);
file_put_contents('plik-csv.txt', $nowyFormat);
Wszystkie powyższe skrypty administracyjne stosują do zapisu danych funkcję file_put_contents(). Dane najpierw są odczytywane z pliku (funkcje file() lub file_get_contents()), następnie podlegają przetworzeniu (pojedyncze wywołanie funkcji gzencode(), tidy_repair_string(), mb_convert_case(), pl_win2iso(), imagepng() lub pętla foreach przetwarzająca kolejne wiersze). Dane po przetworzeniu są umieszczane w napisie i podlegają zapisaniu pojedynczym wywołaniem funkcji file_put_contents().
Zwróćmy jeszcze uwagę na fakt, że korzystniejszym jest jednokrotne administracyjne wykonanie danego przekształcenia niż przetwarzanie pliku przy każdej wizycie.
Najbardziej kontrowersyjnym przypadkiem stosowania plików do przechowywania danych jest zapisywanie informacji w plikach wewnątrz skryptu PHP dostępnego on-line. Zapisywanie danych do pliku w skrypcie on-line podlega najbardziej rygorystycznym ograniczeniom. Organizacja kodu powinna być taka, by bez względu na liczbę równoczesnych wizyt skrypt nigdy nie zniszczył danych zapisanych w pliku, oraz by informacje pochodzące od każdej z wizyt zostały poprawnie zapisane do pliku. Pamiętajmy, że błędy operacji plikowych mogą pojawić się niezależnie, na przykład w wyniku przepełnienia lub awarii dysku. Nie mogąc zagwarantować stuprocentowej poprawności zapisu danych, wymagamy by operacja dostępu do pliku zwracała wartość logiczną informującą o przebiegu całej operacji. Zwróconą wartość true będziemy interpretowali jako gwarancję poprawności wykonanej operacji.
Dodajmy, że blokowanie dostępu do pliku na wyłączność (blokada typu LOCK_EX), wpływa na wykonanie pozostałych skryptów, uzyskujących dostęp do podanego pliku. W związku z tym, czas blokowania pliku należy maksymalnie skrócić. Niedopuszczalnym jest zakładanie blokady na początku wykonania skryptu i zwalnianie jej na samym końcu.
Jako przykład rozważmy skrypt zliczający użytkowników, którzy w danej chwili korzystają z witryny. Zadaniem skryptu jest wyświetlenie (przy każdej wizycie) informacji dotyczącej liczby użytkowników, którzy w danej chwili odwiedzają stronę. Przykładowy wydruk może mieć postać:
Licba użytkowników on-line: 16.
Informacje na temat użytkowników aktualnie odwiedzających stronę będą zapisywane w pliku tekstowym online-datafile.txt o formacie:
adres komputera|moment rozpoczęcia wizyty
Separatorem pól jest znak '|'. Pierwsze z pól jest nazwą lub adresem IP hosta, drugie — momentem rozpoczęcia wizyty mierzonym w sekundach, jakie upłynęły od początku epoki Unixa (tj. godziny 00:00 dnia 1 stycznia 1970 r.). Przykładowe wpisy mogą być następujące:
localhost|1114155826 192.168.0.15|1114155850 192.168.0.107|1114155990 gdzies.w.sieci.com|1114155994
Przy wizycie na stronie w pliku tym należy umieścić wpis na temat bieżącej wizyty: adres IP hosta, z jakiego pochodzi wizyta pobieramy ze zmiennej $_SERVER['REMOTE_ADDR'], a moment wizyty ustalamy wywołując funkcję time(). Stosując funkcję gethostbyaddr() zamieniamy adres IP na nazwę hosta (o ile DNS zawiera stosowne informacje; w przeciwnym razie zmienna $adr będzie zawierała adres IP):
$adr = gethostbyaddr($_SERVER['REMOTE_ADDR']);
Przetwarzanie zawartości pliku online-datafile.txt musi w tym przypadku uwzględniać wszystkie możliwe zabezpieczenia. Listing 3 przedstawia funkcję readWriteDataFile() (jest to metoda klasy onLineUsers). Funkcja ta demonstruje schemat przetwarzania pliku tekstowego w skrypcie, który zapisuje dane do pliku i może być wykonywany współbieżnie.
function readWriteDataFile()
{
$fd = fopen($this->FDataFile, 'rb+');
if (!$fd) {
return false;
};
$ileprob = 100;
$i = 0;
$lock = flock($fd, LOCK_EX);
while ((!$lock) && ($i < $ileprob)) {
$lock = flock($fd, LOCK_EX);
$i++;
};
if (!$lock) {
fclose($fd);
return false;
};
clearstatcache();
$f_size = filesize($this->FDataFile);
if ($f_size === false) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
}
$contents = fread($fd, $f_size);
if ($contents === false) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
}
$c_size = strlen($contents);
if ($c_size != $f_size) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
//=================================================
//przetwarzanie danych - początek
//zawartośc pliku jest w zmiennej $contents
$dozapisu = $this->processData($contents);
//przetwarzanie danych - koniec
//treść jaką należy zapisać w pliku jest w zmiennej $dozapisu
//=================================================
if (!ftruncate($fd, 0)) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
if (fseek($fd, 0) != 0) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
$c_size = strlen($dozapisu);
$tmpresult = fwrite($fd , $dozapisu);
if ($c_size != $tmpresult) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
if (!fflush($fd)) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
if (!flock($fd, LOCK_UN)) {
fclose($fd);
return false;
};
if (!fclose($fd)) {
return false;
};
return true;
}
Listing 3. Odczyt, przetwarzanie i zapis danych w skrypcie dostępnym on-line.
Kolejno wykonujemy operacje:
Wewnątrz funkcji readWriteDataFile() badamy wynik każdej z powyższych operacji. W przypadku niepowodzenia zwracamy wartość false. W przeciwnym razie, czyli wtedy, gdy wszystkie operacje się powiodły, zwracamy wynik true.
Wywołanie funkcji readWriteDataFile() poprzedzamy instrukcją, która uniemożliwi przerwanie wykonywania skryptu przez użytkownika (np. przez naciśnięcie przycisku Stop przeglądarki):
$old_abort = ignore_user_abort(true); $this->FState = $this->readWriteDataFile(); ignore_user_abort($old_abort);
Opisana metoda stanowi uniwersalny schemat przetwarzania pliku tekstowego, w skrypcie wykonywanym podczas każdej wizyty na stronie.
W dokumentacji funkcji flock() znajduje się informacja, że w przypadku korzystania z blokad dostępu, wszystkie skrypty powinny działać w spójny sposób. Oznacza to, że nie mogą pojawić się skrypty, które uzyskują dostęp do pliku z pominięciem mechanizmu blokady.
Zatem każdy skrypt, który zapisuje informacje w podanym pliku, musi zakładać — na czas realizacji operacji dostępu do pliku — blokadę LOCK_EX. Natomiast skrypty, odczytujące podany pliki powinny zakładać blokadę do odczytu (tj. LOCK_SH).
Listing 4 przedstawia metodę readDataFile() klasy onLineUsers. Metoda ta odczytuje plik online-datafile.txt przechowujący dane o użytkownikach korzystających w danej chwili z witryny.
function readDataFile()
{
$fd = fopen($this->FDataFile, 'rb');
if (!$fd) {
return false;
};
$ileprob = 100;
$i = 0;
$lock = flock($fd, LOCK_SH);
while ((!$lock) && ($i < $ileprob)) {
$lock = flock($fd, LOCK_SH);
$i++;
};
if (!$lock) {
fclose($fd);
return false;
};
clearstatcache();
$f_size = filesize($this->FDataFile);
if ($f_size === false) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
}
$contents = fread($fd, $f_size);
if ($contents === false) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
}
$c_size = strlen($contents);
if ($c_size != $f_size) {
flock($fd, LOCK_UN);
fclose($fd);
return false;
};
if (!flock($fd, LOCK_UN)) {
fclose($fd);
return false;
};
if (!fclose($fd)) {
return false;
};
//=================================================
//odczytane dane są w zmiennej $contents
$this->stringToArray($contents);
return true;
}
Listing 4. Odczyt danych z pliku dostępnego w trybie odczyt pliku zapisywanego.
W tym przypadku schemat przetwarzania jest nieco prostszy i przebiega następująco:
Z racji na specyficzny sposób wykonywania operacji odczytu i zapisu danych w plikach (tj. pojedyncze wywołanie funkcji fread() czy fwrite) warto przygotować uniwersalne, dwukierunkowe funkcje, konwertujące napis w tablicę dwuwymiarową. Funkcje te, o nazwach string2HArray(), string2VArray(), vArray2String() oraz hArray2String() zostały zapisane w pliku vh-array.inc.php.
Przykładowe wywołanie ma postać:
$plk = file_get_contents('dane.txt');
$tab = string2VArray($plk, '*');
var_dump($tab);
Zawartość pliku dane.txt, po odczytaniu funkcją file_get_contents(), zostaje „pokrojona” znakiem '*'. Wynik krojenia jest zwracany przez funkcję string2VArray() i umieszczany w zmiennej $tab.
Funkcje krojące podany plik (czyli string2VArray() oraz string2HArray()) zwracają wymiary tablic powstałych przez pokrojenie zadanego napisu oraz ich wymiary. Wymiary możemy przypisać do osobnych zmiennych stosując instrukcję:
list($wiersze, $kolumny, $dane) = string2VArray($plk, '*');
Jako demonstrację użycia funkcji wykonujących dwukierunkowe transformacje tablic w napisy, przyjrzyjmy się witrynie zliczającej liczbę pobrań poszczególnych stron.
Na rysunku 1 został przedstawiony wygląd witryny. Menu, dostępne z lewej strony, zawiera tytuły podstron (są to tytuły kolęd). Wybrana treść jest widoczna w środkowej części strony, zaś statystyka pobrań poszczególnych podstron stanowi treść prawej kolumny.
Rysunek 1. Wygląd witryny ze statystyką pobrań
Operacje plikowe są realizowane przez metodę wczytajIZapiszDane() klasy Menu. Jej szkielet jest identyczny, jak metoda readWriteDataFile() przedstawiona na listingu 3. Metody te różnią się jedynie fragmentem, który wykonuje przetwarzanie danych odczytanych z pliku. Przetwarzanie wykonywane na stronie ze statystyką pobrań stosuje funkcje konwersji napisu w tablicę pionową oraz tablicy pionowej w napis i jest przedstawione na listingu 5
function wczytajIZapiszDane()
{
...
//===========================
//przetwarzanie - początek
//wczytana zawartośc pliku w zmiennej $contents
list($ilewierszy, $ilekolumn, $tablica) =
string2VArray($contents, $this->FSeparator);
$this->FDane = $tablica;
//dopiero teraz znamy liczbę linii w pliku
//walidujemy podaną liczbę
if (!ivpifr($this->FId, 0, $ilewierszy - 1)) {
$this->FId = 0;
};
$this->FDane[2][$this->FId] = $this->FDane[2][$this->FId] + 1;
$dozapisu =
vArray2String(
$this->FDane,
$ilewierszy,
$ilekolumn,
$this->FSeparator
);
//przetwarzanie - koniec
//treść, jaką należy zapisać w pliku,
// jest w zmiennej $dozapisu
//===========================
...
}
Listing 5. Statystyka pobrań kolęd: metoda wczytajIZapiszDane() klasy Menu.
Pomimo wielu potencjalnych zastosowań, skrypty PHP wykorzystujące dostęp do plików jak również same pliki zawierające dane, możemy poklasyfikować pod względem ewentualnych niebezpieczeństw. Wyróżnienie opisanych czterech przypadków istotnie zmniejsza ryzyko zarówno utraty zapisywanych danych jak i zniszczenia samych plików.
Podane rozwiązania, w szczególności zapisywanie danych do plików w skryptach dostępnych on-line, może stanowić punkt wyjściowy wielu różnych zastosowań. Rozwiązanie to sprawdza się w przypadku plików sięgających kilku megabajtów, które mają interpretację jednej tabeli danych. Wystarcza to do przygotowywania różnych statystyk pobrań, liczników jak i ankiet. Jeśli rozmiar danych, relacje pomiędzy poszczególnymi rekordami czy metody porządkowania wyników stają się bardziej skomplikowane, wtedy zdecydowanie należy rozważyć zastąpienie plików bazą danych.
Tabela 2. Przykłady do pobrania