PHP/System plików

< PHP
Poprzedni rozdział: Internacjonalizacja
Następny rozdział: Data i czas

System plików edytuj

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.

Odczyt danych edytuj

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
	// 1.1 - Tworzymy odwołanie do pliku
	$_Uchwyt = fopen('plik.txt', 'r');
	
	// 1.2 - Wykonujemy kod dopóki skrypt nie napotka końca pliku
	while(!feof($_Uchwyt))
	{
		// 1.2.1 - Czytamy z pliku jeden kilobajt (1024 B)
		echo fread($_Uchwyt, 1024);	
	}
	
	// 1.3 - Zamykamy plik
	fclose($_Uchwyt);
?>

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().

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
	// 1.1 - Otwieramy
	$_Uchwyt = fopen('plik.txt', 'r');
	
	// 1.2 - Czytamy całą zawartość pliku
	echo fread($_Uchwyt, filesize('plik.txt'));
	
	// 1.3 - Zamykamy
	fclose($_Uchwyt);

?>

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
	// 1.1 - Otwieramy plik, alarmujemy w wypadku błędu
	$_Uchwyt = @fopen('inny_plik.txt', 'r') or die('Wystąpił błąd.');
	// 1.2 - Czytamy plik
	echo fread($_Uchwyt, filesize('inny_plik.txt'));
	// 1.3 - Zamykamy
	fclose($_Uchwyt);
?>

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
	// 1.1 - Otwieramy, czytamy i zamykamy
	$_TrescPliku = @file_get_contents('plik.txt') or die('Wystąpił błąd.');
	
	// 1.2 - Zwracamy treść pliku
	echo $_TrescPliku;
?>

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
	// 1.1 - Otwieramy plik i czytamy go do tablicy
	$_TrescPliku = @file('plik.txt') or die('Wystąpił błąd.');
	
	// 1.2 - Otwieramy znacznik listy
	echo '<ul>';
	// 1.3 - Dla każdej linii pliku...
	foreach($_TrescPliku as $_Linia)
	{
		// 1.3.1 - ...tworzymy nowy element listy...
		echo '<li>'.$_Linia.'</li>';	
	}
	// 1.4 - ...i zamykamy listę.
	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("", $_TrescPliku);

zamiast np.

implode("\n", $_TrescPliku);

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.

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

<?php
	// 1.1 - Inicjacja zmiennej
	$_Plik='plik.txt';
	// 1.2 - Kontynuujemy jeśli plik istnieje
	if (file_exists($_Plik))
	{
		// 1.2.1 - Czytamy zawartość
		$zawartosc=file($_Plik);
		// 1.2.2 - Sprawdzamy czy plik jest pusty
		if (count($zawartosc)==0)
		{
			die("Plik: $_Plik jest pusty!");
		}
	}
	else
	{
		// 1.2.3 - Jeśli plik nie istnieje wyświetlamy komunikat
		die("Plik: $_Plik nie istnieje!"); 
	}
// Tutaj dalsze operacje na pliku
?>

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 blokach if 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ć.

Zapis danych edytuj

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
	// 1.1 - Otwieranie
	$_Plik = fopen('./plik.txt', 'w');
	// 1.2 - Zapisywanie łańcucha
	fwrite($_Plik, 'To jest nowa zawartość pliku');
	// 1.3 - Zamykanie
	fclose($_Plik);

?>

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
	// 1.1 - Jeśli zapis się powiódł wyświetl komunikat o powodzeniu
	if(file_put_contents('./plik.txt', 'To jest nowa zawartość pliku') != 0)
	{
		echo 'Udało się zapisać nową zawartość 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
	// 1.1 - Jeśli zapis się powiódł wyświetl komunikat o powodzeniu
	if(file_put_contents('./plik.txt', 'Dopisana treść', FILE_APPEND) != 0)
	{
		echo 'Udało się dodać zawartość do pliku.';	
	}

?>

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

Informacje o plikach edytuj

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.

Ścieżki dostępu edytuj

Wydajność i bezpieczeństwo edytuj

Zakończenie edytuj

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.