Lekcja 13: Praca z Plikami i Katalogami w PHP

Witaj w trzynastej lekcji kursu PHP! Po opanowaniu obsługi błędów i wyjątków, przechodzimy do bardzo praktycznego i często wykorzystywanego aspektu programowania: interakcji z systemem plików. Aplikacje internetowe często muszą odczytywać dane z plików (np. pliki konfiguracyjne, szablony, dane CSV), zapisywać informacje (np. logi, dane użytkowników, cache), tworzyć, usuwać lub modyfikować pliki i katalogi. PHP dostarcza bogaty zestaw wbudowanych funkcji do wykonywania tych operacji. W tej lekcji nauczymy się, jak efektywnie pracować z plikami i katalogami.

Podstawowe Informacje o Plikach i Katalogach

Zanim przejdziemy do funkcji PHP, przypomnijmy kilka podstawowych pojęć:

Sprawdzanie Istnienia i Typów Plików/Katalogów

PHP oferuje wiele funkcji do sprawdzania statusu plików i katalogów:

<?php
$plikTestowy = "moj_plik.txt";
$katalogTestowy = "moj_katalog";

// Utworzenie pliku i katalogu do testów (jeśli nie istnieją)
if (!file_exists($plikTestowy)) {
    file_put_contents($plikTestowy, "To jest testowy plik.\nLinia druga.");
}
if (!is_dir($katalogTestowy)) {
    mkdir($katalogTestowy);
}

echo "<h3>Informacje o: $plikTestowy</h3>";
if (file_exists($plikTestowy)) {
    echo "Plik istnieje.<br>";
    if (is_file($plikTestowy)) {
        echo "Jest to zwykły plik.<br>";
    }
    echo "Rozmiar pliku: " . filesize($plikTestowy) . " bajtów.<br>";
    echo "Typ pliku: " . filetype($plikTestowy) . "<br>";
    echo "Ostatnia modyfikacja: " . date("Y-m-d H:i:s", filemtime($plikTestowy)) . "<br>";
    echo "Czy odczytywalny? " . (is_readable($plikTestowy) ? "Tak" : "Nie") . "<br>";
    echo "Czy zapisywalny? " . (is_writable($plikTestowy) ? "Tak" : "Nie") . "<br>";
} else {
    echo "Plik nie istnieje.<br>";
}

echo "<h3>Informacje o: $katalogTestowy</h3>";
if (file_exists($katalogTestowy)) {
    echo "Katalog istnieje.<br>";
    if (is_dir($katalogTestowy)) {
        echo "Jest to katalog.<br>";
    }
    echo "Typ: " . filetype($katalogTestowy) . "<br>";
} else {
    echo "Katalog nie istnieje.<br>";
}

// Czyszczenie po testach (opcjonalne)
// unlink($plikTestowy);
// rmdir($katalogTestowy);
?>

Odczyt Zawartości Plików

PHP oferuje kilka sposobów na odczytanie zawartości plików tekstowych.

1. file_get_contents(string $sciezka, bool $use_include_path = false, ?resource $context = null, int $offset = 0, ?int $maxlen = null): string|false

Jest to najprostszy i często preferowany sposób na wczytanie całej zawartości pliku do stringa. Funkcja ta jest bardzo wydajna dla mniejszych plików.

<?php
$plikDoOdczytu = "moj_plik.txt";
if (!file_exists($plikDoOdczytu)) file_put_contents($plikDoOdczytu, "Pierwsza linia pliku.\nDruga linia pliku.");

$zawartosc = file_get_contents($plikDoOdczytu);
if ($zawartosc !== false) {
    echo "<h3>Zawartość pliku ($plikDoOdczytu) odczytana przez file_get_contents:</h3>";
    echo "<pre>" . htmlspecialchars($zawartosc) . "</pre>";
} else {
    echo "Nie udało się odczytać pliku $plikDoOdczytu.<br>";
}

// Odczyt fragmentu pliku (od PHP 5.1)
// $fragment = file_get_contents($plikDoOdczytu, false, null, 10, 5); // Od 10. bajtu, maksymalnie 5 bajtów
// if ($fragment !== false) {
//     echo "Fragment pliku: " . htmlspecialchars($fragment) . "<br>";
// }
?>

2. file(string $sciezka, int $flags = 0, ?resource $context = null): array|false

Funkcja file() wczytuje cały plik do tablicy, gdzie każdy element tablicy odpowiada jednej linii z pliku (wraz ze znakiem nowej linii na końcu). Jest to przydatne, gdy chcemy przetwarzać plik linia po linii.

Flagi (opcjonalne):

<?php
$plikDoOdczytuLiniami = "moj_plik.txt";

$linie = file($plikDoOdczytuLiniami);
if ($linie !== false) {
    echo "<h3>Zawartość pliku ($plikDoOdczytuLiniami) odczytana przez file() jako tablica linii:</h3>";
    echo "<pre>";
    print_r(array_map('htmlspecialchars', $linie));
    echo "</pre>";

    echo "<h4>Przetwarzanie linii:</h4>";
    foreach ($linie as $numerLinii => $linia) {
        echo "Linia " . ($numerLinii + 1) . ": " . htmlspecialchars(trim($linia)) . "<br>";
    }
} else {
    echo "Nie udało się odczytać pliku $plikDoOdczytuLiniami.<br>";
}

// Z flagami
$linieBezEnterow = file($plikDoOdczytuLiniami, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// echo "<h3>Linie bez enterów i pustych:</h3><pre>"; print_r(array_map('htmlspecialchars', $linieBezEnterow)); echo "</pre>";
?>

3. Niskopoziomowe operacje na plikach (fopen, fread, fgets, fclose)

Dla większej kontroli nad procesem odczytu (np. przy pracy z bardzo dużymi plikami, które nie mieszczą się w pamięci, lub przy odczycie binarnym) można używać funkcji niskopoziomowych:

<?php
$plikDoNiskopoziomowegoOdczytu = "moj_plik.txt";

$uchwyt = fopen($plikDoNiskopoziomowegoOdczytu, "r"); // Otwarcie do odczytu ("r")

if ($uchwyt) {
    echo "<h3>Odczyt pliku ($plikDoNiskopoziomowegoOdczytu) za pomocą fgets():</h3>";
    while (($linia = fgets($uchwyt)) !== false) {
        echo htmlspecialchars($linia) . "<br>";
    }

    // Po odczytaniu do końca, trzeba przewinąć wskaźnik, aby czytać ponownie
    rewind($uchwyt); // Przewija wskaźnik na początek pliku

    echo "<h3>Odczyt pliku ($plikDoNiskopoziomowegoOdczytu) za pomocą fread():</h3>";
    $calaZawartosc = fread($uchwyt, filesize($plikDoNiskopoziomowegoOdczytu));
    echo "<pre>" . htmlspecialchars($calaZawartosc) . "</pre>";

    if (!fclose($uchwyt)) {
        echo "Nie udało się zamknąć pliku!<br>";
    }
} else {
    echo "Nie udało się otworzyć pliku $plikDoNiskopoziomowegoOdczytu do odczytu.<br>";
}
?>

Tryby otwarcia pliku (najważniejsze):

Zapisywanie do Plików

1. file_put_contents(string $sciezka, mixed $dane, int $flags = 0, ?resource $context = null): int|false

Jest to najprostszy sposób na zapisanie danych (stringa, tablicy lub zasobu strumienia) do pliku. Domyślnie nadpisuje plik, jeśli istnieje, lub tworzy nowy.

Flagi (opcjonalne):

<?php
$plikDoZapisu = "nowy_plik.txt";
$daneDoZapisu = "To są dane do zapisania w pliku.\nKolejna linia danych.";

// Zapis (nadpisanie lub utworzenie)
if (file_put_contents($plikDoZapisu, $daneDoZapisu) !== false) {
    echo "Dane zostały pomyślnie zapisane do pliku $plikDoZapisu.<br>";
    echo "<pre>" . htmlspecialchars(file_get_contents($plikDoZapisu)) . "</pre>";
} else {
    echo "Nie udało się zapisać danych do pliku $plikDoZapisu.<br>";
}

// Dopisywanie do pliku
$daneDoDopisania = "To jest dopisana linia.\n";
if (file_put_contents($plikDoZapisu, $daneDoDopisania, FILE_APPEND | LOCK_EX) !== false) {
    echo "Dane zostały pomyślnie dopisane do pliku $plikDoZapisu.<br>";
    echo "<pre>" . htmlspecialchars(file_get_contents($plikDoZapisu)) . "</pre>";
} else {
    echo "Nie udało się dopisać danych do pliku $plikDoZapisu.<br>";
}

// Zapis tablicy (każdy element jako nowa linia)
// $tablicaDanych = ["Linia 1", "Linia 2", "Linia 3"];
// file_put_contents("plik_z_tablicy.txt", implode("\n", $tablicaDanych));
?>

2. Niskopoziomowe operacje zapisu (fopen, fwrite, fputs, fclose)

Podobnie jak przy odczycie, można używać funkcji niskopoziomowych do zapisu, co daje większą kontrolę.

<?php
$plikDoNiskopoziomowegoZapisu = "zapis_niskopoziomowy.txt";
$dane1 = "Pierwsza porcja danych do zapisu.\n";
$dane2 = "Druga porcja danych.\n";

// Otwarcie do zapisu (tryb "w" - nadpisuje lub tworzy)
$uchwytZapisu = fopen($plikDoNiskopoziomowegoZapisu, "w");
if ($uchwytZapisu) {
    fwrite($uchwytZapisu, $dane1);
    fwrite($uchwytZapisu, $dane2);
    fclose($uchwytZapisu);
    echo "Dane zapisane niskopoziomowo do $plikDoNiskopoziomowegoZapisu:<br>";
    echo "<pre>" . htmlspecialchars(file_get_contents($plikDoNiskopoziomowegoZapisu)) . "</pre>";
} else {
    echo "Nie udało się otworzyć pliku $plikDoNiskopoziomowegoZapisu do zapisu.<br>";
}

// Otwarcie do dopisywania (tryb "a")
$daneDoDopisaniaNisko = "Dopisane dane niskopoziomowo.\n";
$uchwytDopisywania = fopen($plikDoNiskopoziomowegoZapisu, "a");
if ($uchwytDopisywania) {
    fwrite($uchwytDopisywania, $daneDoDopisaniaNisko);
    fclose($uchwytDopisywania);
    echo "Dane dopisane niskopoziomowo do $plikDoNiskopoziomowegoZapisu:<br>";
    echo "<pre>" . htmlspecialchars(file_get_contents($plikDoNiskopoziomowegoZapisu)) . "</pre>";
} else {
    echo "Nie udało się otworzyć pliku $plikDoNiskopoziomowegoZapisu do dopisywania.<br>";
}
?>

Operacje na Plikach i Katalogach

<?php
$oryginal = "oryginalny_plik.txt";
$kopia = "kopia_pliku.txt";
$nowaNazwa = "zmieniona_nazwa.txt";
$katalogDoStworzenia = "nowy_folder/podfolder";

if (!file_exists($oryginal)) file_put_contents($oryginal, "Zawartość oryginalnego pliku.");

// Kopiowanie
if (copy($oryginal, $kopia)) {
    echo "Plik $oryginal skopiowany do $kopia.<br>";
} else {
    echo "Nie udało się skopiować pliku.<br>";
}

// Zmiana nazwy
if (rename($kopia, $nowaNazwa)) {
    echo "Plik $kopia zmieniono na $nowaNazwa.<br>";
} else {
    echo "Nie udało się zmienić nazwy pliku.<br>";
}

// Usuwanie pliku
if (file_exists($nowaNazwa) && unlink($nowaNazwa)) {
    echo "Plik $nowaNazwa usunięty.<br>";
}
if (file_exists($oryginal) && unlink($oryginal)) {
    echo "Plik $oryginal usunięty.<br>";
}

// Tworzenie katalogu rekursywnie
if (!is_dir($katalogDoStworzenia) && mkdir($katalogDoStworzenia, 0755, true)) {
    echo "Katalog $katalogDoStworzenia utworzony.<br>";
} else if (is_dir($katalogDoStworzenia)) {
    echo "Katalog $katalogDoStworzenia już istnieje.<br>";
} else {
    echo "Nie udało się utworzyć katalogu $katalogDoStworzenia.<br>";
}

// Usuwanie katalogu (musi być pusty, najpierw podfolder, potem nadrzędny)
if (is_dir($katalogDoStworzenia) && rmdir($katalogDoStworzenia)) {
    echo "Katalog $katalogDoStworzenia usunięty.<br>";
    // Usunięcie nadrzędnego, jeśli też jest pusty
    if (rmdir(dirname($katalogDoStworzenia))) {
        echo "Katalog " . dirname($katalogDoStworzenia) . " usunięty.<br>";
    }
}
?>

Praca z Katalogami - Listowanie Zawartości

<?php
$katalogDoListowania = "."; // Bieżący katalog

echo "<h3>Zawartość katalogu ".$katalogDoListowania." (scandir):</h3>";
$elementy = scandir($katalogDoListowania);
if ($elementy !== false) {
    echo "<ul>";
    foreach ($elementy as $element) {
        if ($element != "." && $element != "..") { // Pomijamy . i ..
            echo "<li>" . htmlspecialchars($element) . (is_dir($katalogDoListowania . DIRECTORY_SEPARATOR . $element) ? " (Katalog)" : " (Plik)") . "</li>";
        }
    }
    echo "</ul>";
} else {
    echo "Nie udało się odczytać zawartości katalogu.<br>";
}

// Użycie DirectoryIterator (bardziej obiektowo)
echo "<h3>Zawartość katalogu ".$katalogDoListowania." (DirectoryIterator):</h3>";
try {
    $iterator = new DirectoryIterator($katalogDoListowania);
    echo "<ul>";
    foreach ($iterator as $fileinfo) {
        if (!$fileinfo->isDot()) { // Pomijamy . i ..
            echo "<li>" . htmlspecialchars($fileinfo->getFilename());
            if ($fileinfo->isDir()) echo " (Katalog)";
            if ($fileinfo->isFile()) echo " (Plik)";
            echo "</li>";
        }
    }
    echo "</ul>";
} catch (Exception $e) {
    echo "Błąd iteratora: " . $e->getMessage() . "<br>";
}

// Stała DIRECTORY_SEPARATOR jest ważna dla kompatybilności między systemami ( / vs \ )
?>

Ścieżki i Informacje o Ścieżkach

<?php
$pelnaSciezka = "/var/www/html/projekt/index.php";

echo "<h3>Informacje o ścieżce: $pelnaSciezka</h3>";
echo "basename: " . basename($pelnaSciezka) . "<br>"; // index.php
echo "basename (.php): " . basename($pelnaSciezka, ".php") . "<br>"; // index
echo "dirname: " . dirname($pelnaSciezka) . "<br>"; // /var/www/html/projekt

$info = pathinfo($pelnaSciezka);
echo "pathinfo (tablica): <pre>"; print_r($info); echo "</pre>";
/*
Array
(
    [dirname] => /var/www/html/projekt
    [basename] => index.php
    [extension] => php
    [filename] => index
)
*/
echo "pathinfo (extension): " . pathinfo($pelnaSciezka, PATHINFO_EXTENSION) . "<br>"; // php

// Dla realpath, plik/katalog musi istnieć
// $sciezkaRelatywna = "../lessons_php/./01-wprowadzenie-do-php_content_draft.html"; // przykład
// if (file_exists($sciezkaRelatywna)) {
//     echo "realpath: " . realpath($sciezkaRelatywna) . "<br>";
// } else {
//     echo "Plik dla realpath nie istnieje: $sciezkaRelatywna<br>";
// }

echo "Bieżący katalog roboczy (getcwd): " . getcwd() . "<br>";
?>

Podsumowanie Lekcji

W tej lekcji nauczyliśmy się podstaw pracy z systemem plików w PHP. Poznaliśmy funkcje do sprawdzania istnienia i typów plików/katalogów, odczytywania zawartości plików na różne sposoby (file_get_contents, file, oraz niskopoziomowe fopen/fread/fgets), zapisywania danych do plików (file_put_contents, fopen/fwrite), a także wykonywania operacji takich jak kopiowanie, zmiana nazwy, usuwanie plików i katalogów, oraz zmiana uprawnień. Omówiliśmy również sposoby listowania zawartości katalogów i uzyskiwania informacji o ścieżkach.

Umiejętność manipulowania plikami i katalogami jest niezbędna w wielu zastosowaniach webowych, od prostych skryptów po zaawansowane systemy zarządzania treścią. W następnej lekcji zajmiemy się obsługą formularzy HTML i przetwarzaniem danych przesyłanych przez użytkownika za pomocą metod GET i POST.


Zadanie praktyczne

Napisz skrypt PHP, który:

  1. Tworzy katalog o nazwie dane_uzytkownika (jeśli nie istnieje).
  2. Wewnątrz tego katalogu tworzy plik o nazwie profil.txt.
  3. Zapisuje do pliku profil.txt następujące informacje (każda w nowej linii): Imię: [Twoje Imię], Email: [Twój Email], Data rejestracji: [Aktualna data i czas].
  4. Odczytuje zawartość pliku profil.txt i wyświetla ją na stronie.
  5. Wyświetla rozmiar pliku profil.txt w bajtach oraz datę jego ostatniej modyfikacji.
  6. Tworzy kopię pliku profil.txt o nazwie profil_backup.txt w tym samym katalogu.
  7. Listuje zawartość katalogu dane_uzytkownika (tylko nazwy plików).

Pokaż przykładowe rozwiązanie
<?php
$katalogUzytkownika = "dane_uzytkownika";
$plikProfilu = $katalogUzytkownika . DIRECTORY_SEPARATOR . "profil.txt";
$plikBackup = $katalogUzytkownika . DIRECTORY_SEPARATOR . "profil_backup.txt";

// 1. Tworzenie katalogu
if (!is_dir($katalogUzytkownika)) {
    if (mkdir($katalogUzytkownika, 0755)) {
        echo "Katalog ".$katalogUzytkownika." utworzony.<br>";
    } else {
        die("Nie udało się utworzyć katalogu ".$katalogUzytkownika.".<br>");
    }
} else {
    echo "Katalog ".$katalogUzytkownika." już istnieje.<br>";
}

// 2. i 3. Tworzenie i zapis do pliku profil.txt
$imie = "Jan"; // Zastąp swoimi danymi
$email = "jan.kowalski@example.com";
$dataRejestracji = date("Y-m-d H:i:s");

$daneProfilu = "Imię: $imie\n";
$daneProfilu .= "Email: $email\n";
$daneProfilu .= "Data rejestracji: $dataRejestracji\n";

if (file_put_contents($plikProfilu, $daneProfilu) !== false) {
    echo "Dane zapisane do pliku $plikProfilu.<br>";
} else {
    echo "Nie udało się zapisać danych do pliku $plikProfilu.<br>";
}

// 4. Odczyt i wyświetlenie zawartości
if (file_exists($plikProfilu)) {
    $zawartoscProfilu = file_get_contents($plikProfilu);
    echo "<h4>Zawartość pliku $plikProfilu:</h4>";
    echo "<pre>" . htmlspecialchars($zawartoscProfilu) . "</pre>";
} else {
    echo "Plik $plikProfilu nie istnieje.<br>";
}

// 5. Wyświetlenie rozmiaru i daty modyfikacji
if (file_exists($plikProfilu)) {
    echo "Rozmiar pliku $plikProfilu: " . filesize($plikProfilu) . " bajtów.<br>";
    echo "Data ostatniej modyfikacji $plikProfilu: " . date("Y-m-d H:i:s", filemtime($plikProfilu)) . "<br>";
}

// 6. Tworzenie kopii zapasowej
if (file_exists($plikProfilu)) {
    if (copy($plikProfilu, $plikBackup)) {
        echo "Utworzono kopię zapasową: $plikBackup.<br>";
    } else {
        echo "Nie udało się utworzyć kopii zapasowej.<br>";
    }
}

// 7. Listowanie zawartości katalogu
echo "<h4>Zawartość katalogu $katalogUzytkownika:</h4>";
if (is_dir($katalogUzytkownika)) {
    $elementyKatalogu = scandir($katalogUzytkownika);
    if ($elementyKatalogu !== false) {
        echo "<ul>";
        foreach ($elementyKatalogu as $element) {
            if ($element != "." && $element != "..") {
                if (is_file($katalogUzytkownika . DIRECTORY_SEPARATOR . $element)) {
                    echo "<li>" . htmlspecialchars($element) . "</li>";
                }
            }
        }
        echo "</ul>";
    } else {
        echo "Nie udało się odczytać zawartości katalogu $katalogUzytkownika.<br>";
    }
} else {
    echo "Katalog $katalogUzytkownika nie istnieje.<br>";
}

// Opcjonalne czyszczenie po zadaniu:
// if (file_exists($plikProfilu)) unlink($plikProfilu);
// if (file_exists($plikBackup)) unlink($plikBackup);
// if (is_dir($katalogUzytkownika)) rmdir($katalogUzytkownika);
?>
            

Zadanie do samodzielnego wykonania

Napisz skrypt, który rekursywnie listuje wszystkie pliki i podkatalogi w danym katalogu (np. w bieżącym katalogu skryptu). Dla każdego elementu wyświetl jego pełną ścieżkę oraz informację, czy jest to plik czy katalog. Możesz do tego użyć funkcji rekurencyjnej lub obiektów RecursiveDirectoryIterator i RecursiveIteratorIterator.


FAQ - Praca z Plikami i Katalogami

Jaka jest różnica między ścieżką absolutną a względną?

Ścieżka absolutna zaczyna się od katalogu głównego systemu plików (np. / w Linuksie, C:\ w Windows) i jednoznacznie identyfikuje lokalizację. Ścieżka względna jest określana w odniesieniu do bieżącego katalogu roboczego skryptu PHP.

Do czego służy stała DIRECTORY_SEPARATOR?

Stała DIRECTORY_SEPARATOR zawiera znak separatora katalogów specyficzny dla systemu operacyjnego, na którym działa PHP (/ dla systemów uniksowych, \ dla Windows). Używanie jej zamiast hardkodowania / lub \ zapewnia większą przenośność kodu między platformami.

Jak sprawdzić uprawnienia do pliku przed próbą zapisu?

Można użyć funkcji is_writable("sciezka/do/pliku"). Zwróci ona true, jeśli plik istnieje i proces PHP ma uprawnienia do zapisu, a false w przeciwnym razie. Podobnie is_readable() dla odczytu.

Czy file_get_contents() można używać do odczytu zdalnych plików (URL)?

Tak, jeśli opcja konfiguracyjna allow_url_fopen jest włączona w php.ini (domyślnie jest), file_get_contents() może odczytywać zawartość z adresów URL (np. http://example.com/strona.html). Należy jednak pamiętać o potencjalnych problemach z bezpieczeństwem i wydajnością.

Jak bezpiecznie obsługiwać przesyłane przez użytkownika pliki?

Obsługa przesyłanych plików (uploads) wymaga szczególnej ostrożności. Należy walidować typ pliku, rozmiar, sprawdzać rozszerzenie, używać funkcji move_uploaded_file() zamiast rename()/copy(), oraz przechowywać przesyłane pliki poza głównym katalogiem dostępnym przez WWW, jeśli to możliwe. Temat ten zostanie szerzej omówiony w lekcji o przesyłaniu plików.

Co to jest "race condition" przy operacjach na plikach?

Race condition (wyścig) może wystąpić, gdy wiele procesów próbuje jednocześnie modyfikować ten sam plik. Na przykład, jeden proces sprawdza istnienie pliku (file_exists), a zanim zdąży go utworzyć, inny proces robi to samo. Aby temu zapobiegać, można używać blokad plików (np. flaga LOCK_EX w file_put_contents lub funkcja flock()).

Jak usunąć katalog, który nie jest pusty?

Funkcja rmdir() usuwa tylko puste katalogi. Aby usunąć katalog z zawartością, należy najpierw rekursywnie usunąć wszystkie pliki i podkatalogi wewnątrz niego, a dopiero potem sam katalog. Można napisać własną funkcję rekurencyjną lub użyć gotowych rozwiązań z bibliotek.