PHP/System plików

Z Wikibooks, biblioteki wolnych podręczników.

< PHP

Poprzedni rozdział: Internacjonalizacja Spis treści Następny rozdział: Wstęp do baz danych

Spis treści

[edytuj] System plików

Przez wcześniejsze rozdziały często przewijały się nam różne funkcje odczytu danych z dysku twardego. Przyszedł czas na zebranie informacji o nich oraz ich usystematyzowanie. Pierwszą rzeczą, o której należy pamiętać, jest wydajność. Wszelkie odwołania do systemu plików są dosyć powolne i często stanowią nawet wąskie gardło w szybkości naszego kodu. Dlatego powinieneś starać się wykonywać ich tak mało, jak tylko się da i buforować wyniki działania niektórych z nich, aby późniejszy kod mógł odwoływać się do nich.

[edytuj] Odczyt danych

Zanim zaczniemy, utwórz sobie plik plik.txt z jakąś długą zawartością (najlepiej w kilku linijkach).

Zawartość pliku można odczytać w PHP na kilka sposobów. Oto pierwszy z nich, wywodzący się jeszcze z języka C:

<?php

	$f = fopen('plik.txt', 'r');
	
	while(!feof($f))
	{
		echo fread($f, 1024);	
	}
	
	fclose($f);
?>

Każdy dostęp do pliku musi rozpocząć się od jego otwarcia. Zadaniem tym zajmuje się funkcja fopen(). Parametr r nakazuje otwarcie pliku do odczytu. Następnie w pętli pobieramy plik po kawałkach o wielkości jednego kilobajta. W ten sposób dane mogą być przetwarzane "równolegle" z odczytem. Funkcja feof() służy do sprawdzenia, czy osiągnęliśmy koniec pliku. Po zakończonej pracy nasze połączenie z plikiem trzeba zamknąć. Odpowiada za to fclose().

Uwaga! Uwaga!
Zawsze zamykaj swoje pliki. Inaczej możesz zablokować innych użytkowników korzystających ze strony!

Z powyższego kodu możemy wyrzucić pętlę i pobrać wszystko za jednym zamachem. Wystarczy tylko użyć funkcji filesize(), aby podała nam rozmiar pliku:

<?php
	$f = fopen('plik.txt', 'r');
	
	echo fread($f, filesize('plik.txt'));
	
	fclose($f);
?>

Zwróćmy uwagę na jakość podanych przykładów. Zmień nazwę plików, do których się odwołujemy, na jakiś nieistniejący. Oba skrypty wtedy zgłupieją. Pierwszy zaleje nas falą ostrzeżeń przez 30 sekund (potem przestaną się pojawiać), drugi zrobi ich "tylko" kilka (przyczyną jest brak pętli). Dlatego powinniśmy tak przygotować wszystko, abyśmy sami panowali nad komunikatami. Czas stworzyć prymitywną obsługę błędów. Wykorzystamy tutaj operator @, aby zagłuszyć funkcję fopen() i sprawdzić zwracany wynik. Powinna ona zwrócić nam połączenie z plikiem, tj. wartość typu Resource. Zobaczmy:

<?php

	$f = @fopen('inny_plik.txt', 'r') or die('Wystąpił błąd.');
	
	echo fread($f, filesize('inny_plik.txt'));
	
	fclose($f);
?>
Uwaga! Uwaga!
Jeśli skrypt odczytujący zacznie się dziwnie zachowywać, pierwszym krokiem powinno być tymczasowe usunięcie operatora @ - inaczej nigdy nie dowiesz się, co jest przyczyną problemów!
Porada Porada
W celu uproszczenia twojego kodu możesz napisać sobie własny wariant funkcji fopen() posiadający twoją obsługę błędów. Uprości to zarządzanie kodem projektu.

Od PHP 4.3.0 nie trzeba już rozpisywać się, aby wczytać zawartość pojedynczego pliku. Cała czynność jest zautomatyzowana w funkcji file_get_contents(). Aby tu sprawdzić poprawność otwarcia, wystarczy porównać zwrócony wynik z wartością false, która jest zwracana w przypadku błędu:

<?php

	$tresc = @file_get_contents('plik.txt') or die('Wystąpił błąd.');
	
	echo $tresc;
?>

Pisząc księgę gości, poznaliśmy funkcję file(), która zwracaną zawartość rozbijała od razu na tablicę poszczególnych linijek. Dzięki tej właściwości wyświetlimy plik jako listę wypunktowaną HTML bez większych trudności:

<?php

	$tresc = @file('plik.txt') or die('Wystąpił błąd.');
	
	echo '<ul>';
	foreach($tresc as $linia)
	{
		echo '<li>'.$linia.'</li>';	
	}
	echo '</ul>';
?>

Pamiętaj, że file() nie gubi znaków końca linii - te są nadal zapisane w poszczególnych linijkach. Dlatego gdy będziesz chciał z powrotem połączyć wszystko w całość, powinieneś napisać

implode('', $tresc);

zamiast np.

implode("\n", $tresc);

Pod żadnym pozorem nie odczytuj plików w ten sposób:

implode('', file('plik.txt'));

Sens takiego kodu można streścić w prostym porównaniu: pakować się tylko po to, by się natychmiast rozpakować. Nie służy to niczemu, a konsumuje niezbędny czas. Aby przekonać się, jak mało wydajne jest takie rozwiązanie, spróbuj załadować tak plik tekstowy o wielkości megabajta, następnie powtórz to samo z wykorzystaniem file_get_contents() i porównaj wrażenia.

Porada Jeżeli pragniesz odczytywać pliki binarne, otwieraj je z parametrem rb zamiast r.


Powinniśmy jeszcze wspomnieć o rozwiązaniu tzw. "wg pana od informatyki" (aczkolwiek bardzo go szanujemy). Rozwiązanie najlepiej pokazać na przykładzie:

<?php
	$plik='plik.txt';
	if (file_exists($plik))
	{
		$zawartosc=file($plik);
		if (count($zawartosc)>0)
		{
//nie rob nic. ta linijka jest tu po to, zeby to glupio nie wygladalo.
		}
		else
		{
			die("plik: $plik jest pusty");
		}
	}
	else
	{
		die("plik: $plik nie istnieje"); 
	}
			# ... dalszy kod aplikacji ...
?>

Metoda na "pana nauczyciela" polega na pełnym obsłużeniu wszystkich możliwości jakie mogą wystąpić podczas czytania pliku oraz posłużenia się funkcjami typu file_exists(). Wadą takiego rozwiązania jest jednak to, że pisząc kod możemy zakopać się w if'ach gubiąc główny wątek programu ... a także nie jesteśmy w stanie wymyślić wszystkich możliwych sytuacji, które mogą się zdarzyć.

Uwaga! Uwaga!
Staraj się nie stosować na produkcyjnych stronach rozwiązań w stylu "plik: $plik". W ten sposób ujawniasz napastnikowi strukturę swojego serwisu !

[edytuj] Zapis danych

Zapis danych wygląda analogicznie do odczytu. Różne jest tylko miejsce docelowe danych. Sposób pierwszy polega na otwarciu pliku funkcją fopen() i skorzystaniu z fwrite() do dodania nowej zawartości. Plik otwieramy z parametrem w (nadpisujemy starą zawartość) lub a (dopisujemy coś do pliku). W przypadku operowania danymi binarnymi, dodajemy jeszcze literę b.

<?php

	$f = fopen('./plik.txt', 'w');
	fwrite($f, 'To jest nowa zawartosc pliku');
	fclose($f);

?>

Po uruchomieniu tego skryptu w plik.txt powinna pojawić nam się nowa zawartość. W przypadku pracy na systemie Linux/Unix sprawdź, czy PHP ma uprawnienia do edycji plików w twoim katalogu roboczym.

W PHP 5.0.0 pojawiła się funkcja file_put_contents(), która upraszcza całą sprawę. Zwraca ona liczbę zapisanych do pliku bajtów i możemy wykorzystać to do kontroli, czy operacja dopisywania faktycznie się udała. Funkcja pobiera dwa parametry: nazwę pliku oraz tekst do wpisania i "firmowo" nie zniekształca danych binarnych.

<?php

	if(file_put_contents('./plik.txt', 'To jest nowa zawartosc pliku') != 0)
	{
		echo 'Udalo sie zapisac nowa zawartosc do pliku.';	
	}

?>

Zadajmy sobie pytanie, co jeśli musimy dopisać dodatkową treść. Naturalnie file_put_contents() także to potrafi. Trzeba tylko skorzystać z trzeciego parametru, w którym możemy ustawiać flagi. FILE_APPEND jest tym, czego potrzebujemy.

<?php

	if(file_put_contents('./plik.txt', ' dopisana tresc', FILE_APPEND) != 0)
	{
		echo 'Udało się dodać zawartość do pliku.';	
	}

?>

Ten skrypt będzie już dopisywać dane do pliku, zamiast je nadpisywać.

[edytuj] Informacje o plikach

W wielu przypadkach przydaje się wiedza o tym, co w zasadzie w katalogach mamy. Możemy ją uzyskać, korzystając z rodziny funkcji udostępniających nam różne informacje o plikach. Wszystkie przyjmują za parametr nazwę pliku:

  • is_file() - zwraca true, jeśli obiekt jest plikiem.
  • is_dir() - zwraca true, jeśli obiekt jest katalogiem.
  • is_readable() - zwraca true, jeśli posiadamy prawa do odczytu zawartości obiektu.
  • is_writeable() - zwraca true, jeśli posiadamy prawa do zapisu do obiektu.
  • file_exists() - zwraca true, jeśli plik/katalog istnieje.
  • fowner() - zwraca ID właściciela pliku.
  • fgroup() - zwraca ID grupy, do której plik należy.
  • fperms() - zwraca uprawnienia pliku.
  • filesize() - zwraca wielkość pliku.
  • filemtime() - zwraca czas ostatniej modyfikacji pliku lub false, jeśli nie istnieje.

Przy korzystaniu z nich musimy pamiętać o wydajności. Odczyt wszelkich danych z dysku jest dość powolny, dlatego starajmy się jak najwięcej wycisnąć z pojedynczego wywołania funkcji. Oto przykład: załóżmy, że mamy plik A.txt i na jego podstawie generujemy B.txt zawsze, kiedy ulegnie on zmianie (taki kompilator). Musimy zatem napisać mechanizm sprawdzający, czy można uruchomić kompilację, czy też jest ona zbędna.

<?php

	if(!file_exists('A.txt'))
	{
		die('Plik A.txt nie istnieje!');
	}

	if(file_exists('B.txt'))
	{
		if(filemtime('B.txt') != filemtime('A.txt'))
		{
			echo 'Plik A.txt wymaga kompilacji.';
		}
		else
		{
			echo 'Można czytać z pliku B.txt';
		}	
	}
	else
	{
		echo 'Plik A.txt wymaga kompilacji';
	}

?>

Pozornie wszystko wygląda na poprawne - skrypt prawidłowo raportuje wszystkie sprawy. Jednak robi to zbyt wolno, gdyż przeciążyliśmy go dużą ilością odwołań do dysku twardego. Jeżeli uruchomimy go na witrynie z dużym ruchem, osiągnąłby gorsze wyniki wydajności, niż inne skrypty. Spróbujmy go nieco zmodyfikować. Czy naprawdę potrzebujemy funkcji file_exists()? Okazuje się, że nie. Przecież filemtime() zwróci nam false, jeżeli plik nie będzie istniał i możemy to wykorzystać. Oto poprawiony kod skryptu:

<?php

	$czasA = @filemtime('A.txt');
	
	if($czasA === false)
	{	
		die('Plik A.txt nie istnieje!');
	}
	else
	{
		$czasB = @filemtime('B.txt');
	}

	if($czasB !== false)
	{
		if($czasB != $czasA)
		{
			echo 'Plik A.txt wymaga kompilacji.';
		}
		else
		{
			echo 'Można czytać z pliku B.txt';
		}	
	}
	else
	{
		echo 'Plik A.txt wymaga kompilacji';
	}

?>

Zauważmy, w tym przypadku mamy tylko dwa odwołania do dysku, a jeśli plik A.txt nie będzie istnieć, to nawet jedno! Zamiast wykonywania za każdym razem setek nowych funkcji, wykorzystujemy maksymalnie te dane, które już mamy. To jest właściwa filozofia przy pracy z plikami.

[edytuj] Zawartość katalogów

[edytuj] Ścieżki dostępu

[edytuj] Wydajność i bezpieczeństwo

[edytuj] Zakończenie

Plikom poświęciliśmy naprawdę bardzo duży rozdział. Jednak mało która aplikacja PHP wykorzystuje je jako główne źródło danych dla internauty. Znacznie poważniejszym i mającym większe możliwości narzędziem są bazy danych. Zagadnienie to jest omówione w następnej części podręcznika. Czy jednak pliki należy w takim razie wyrzucić? Nie, ze względu na wydajność. Wbrew pozorom, odczyt rekordów z bazy zazwyczaj jest wolniejszy, niż z pliku i w przypadku elementarnych ustawień aplikacji, które nie wymagają złożonego sortowania oraz stosowania rozbudowanych relacji (np. konfiguracja, dane systemowe), można pokusić się o zastąpienie ich plikami.