11. PHP – Operacje na plikach


Szybki quiz – przypomnienie poprzedniej lekcji

Sprawdź, czy pamiętasz Lekcję 10 (dołączanie plików, ścieżki, katalogi). Odpowiedzi znajdziesz na końcu dodatku.

1. Plik includes/funkcje_zadan.php jest dołączany w index.php i w diagnostyka.php. Której z czterech instrukcji (include, include_once, require, require_once) należy użyć i co stanie się, jeśli użyjesz require bez _once, a oba pliki zostaną załadowane w jednym żądaniu?

2. Projekt leży w katalogu /var/www/taskquest. Co wypisze echo dirname(__DIR__); umieszczone w pliku includes/funkcje_diagnostyczne.php?

3. Co — poza nazwami plików — pojawi się na stronie po uruchomieniu tego kodu i jak się tego pozbyć?

foreach (scandir(__DIR__ . '/data') as $element) {
    echo $element . '<br>';
}

4. Co wypisze var_dump(is_file(__DIR__ . '/data'));, jeśli data to katalog z plikiem zadania.csv? Dlaczego?

5. Skrypt zawiera linię mkdir(__DIR__ . '/logs');. Działa za pierwszym razem, ale przy drugim uruchomieniu pojawia się ostrzeżenie. Dlaczego i jak poprawić kod?

6. Dlaczego w config/config.php i w plikach z funkcjami nie zamykamy tagu ?>?

Odpowiedzi
  1. require_once. Plik z funkcjami zawsze z _once: require dołączyłby go dwa razy, a druga deklaracja tej samej funkcji to błąd krytyczny („Cannot redeclare…”). require zamiast include, bo bez tych funkcji aplikacja nie ma sensu.
  2. /var/www/taskquest. __DIR__ to /var/www/taskquest/includes, a dirname() cofa o jeden poziom — stąd ten zapis w funkcjach diagnostycznych.
  3. Pojawią się dwa dodatkowe wpisy: . (ten katalog) i .. (katalog nadrzędny). scandir() zawsze je zwraca; pomijamy je warunkiem if ($element === '.' || $element === '..') { continue; }.
  4. bool(false) — data jest katalogiem, a is_file() sprawdza, czy ścieżka wskazuje plik. Dla katalogu prawdę zwróci is_dir().
  5. Katalog już istnieje, więc mkdir() zgłasza ostrzeżenie i zwraca false. Wzorzec: najpierw sprawdź, potem działaj — if (!is_dir($katalog)) { mkdir($katalog); }.
  6. Każdy znak po ?> (nawet sam enter) zostaje wysłany do przeglądarki jako treść strony. Przy nagłówkach, cookies i sesjach (Lekcja 14) powoduje to trudne do znalezienia błędy typu „headers already sent”.

TaskQuest

Dodatek

Operacje na plikach w PHP to nic innego jak tworzenie plików z danymi oraz odczytywanie danych z plików. Pełną listę funkcji znajdziecie w dokumentacji PHP – funkcje systemu plików,  poniżej najczęściej wykorzystywane.

Operacje na plikach w PHP mają podobną formę jak w C.

Zapisywanie danych w pliku następuje w 3 etapach:

  1. Otwieramy plik lub go tworzymy
  2. Zapisujemy dane w pliku
  3. Zamykamy plik

Odczytywanie danych z pliku również przebiega 3-etapowo:

  1. Otwieramy plik, a jeżeli takowy nie może zostać otwarty musi zostać oprogramowane zgłoszenie błędu
  2. Odczytujemy dane
  3. Zamykamy plik

file_exists() – sprawdzamy czy plik istnieje

Funkcja sprawdza czy plik lub katalog istnieje.

<?php
if (file_exists('plik.txt')) {
echo "Plik istnieje";
} else {
echo "Plik nie istnieje";
}

// Sprawdź przed operacją
if (!file_exists('dane.txt')) {
file_put_contents('dane.txt', ''); // utwórz pusty plik
}

 is_file() / is_dir() / is_readable() / is_writable()

<?php
$path = 'dokument.pdf';

// Czy to plik?
if (is_file($path)) {
    echo "$path jest plikiem\n";
}

// Czy można odczytać?
if (is_readable($path)) {
    echo "Można odczytać\n";
}

// Czy można zapisać?
if (is_writable($path)) {
    echo "Można zapisać\n";
}

// Czy to katalog?
if (is_dir('uploads')) {
    echo "uploads jest katalogiem\n";
}

fopen(nazwa_pliku, tryb_otwierania) – otwieranie pliku

Przykład:

<?php
$plik = fopen('dane.txt', 'r');

if ($plik === false) {
die('Nie udało się otworzyć pliku');
}

// ... operacje na pliku ...

fclose($plik);

nazwa pliku – musimy pamiętać o ścieżce dostępu do naszego pliku, jeżeli tryb otwierania jest ustawiony na ”r” czyli tylko do odczytu, to za nazwę pliku może służyć adres URL do pliku. Należy uważać, by nie stworzyć pliku o nazwie, która już istnieje – istniejący plik zostanie wtedy trwale usunięty w miejsce nowo stworzonego.

Tryby otwierania pliku

Tryb Opis Wskaźnik Tworzy plik? Obcina plik?
r Tylko odczyt  Początek nie nie
r+ Odczyt i zapis  Początek nie nie
w Tylko zapis  Początek tak tak
w+ Odczyt i zapis  Początek tak tak
a Tylko dopisywanie  Koniec tak nie
a+ Odczyt i dopisywanie  Koniec tak nie
x Tworzenie + zapis  Początek tak (błąd jeśli istnieje) nie
x+ Tworzenie + odczyt/zapis  Początek tak (błąd jeśli istnieje) nie

 

<?php
// Odczyt - plik musi istnieć
$plik = fopen('dane.txt', 'r');

// Zapis - nadpisze cały plik
$plik = fopen('dane.txt', 'w');

// Dopisywanie - na końcu pliku
$plik = fopen('log.txt', 'a');

// Utwórz nowy - błąd jeśli istnieje
$plik = fopen('nowy.txt', 'x');

fclose() – zamykanie pliku

<?php
$plik = fopen('dane.txt', 'r');

// ... operacje ...

fclose($plik);

Zapisywanie danych do pliku

fwrite() / fputs() – zapis danych

fputs() to alias dla fwrite(). Powyższa funkcja zapisuje do pliku tekst, zawarty w zmiennej $tresc. Podczas wywołania funkcji można dodać trzeci, opcjonalny argument, $dlugosc. Jeżeli go zamieścimy, do pliku zostanie zapisanych maksymalnie $dlugosc znaków. Zależnie od trybu otwarcia, zawartość zostanie dopisana na końcu pliku, lub nadpisze istniejącą treść. Jest ona równoważna z instrukcją fputs().

<?php
$plik = fopen('dane.txt', 'w');

if ($plik === false) {
die('Błąd otwarcia pliku');
}

$tresc = "To jest przykładowa treść.\n";
$tresc .= "Druga linia tekstu.\n";

fwrite($plik, $tresc);
fclose($plik);

echo "Dane zapisane!";

Poniżej przykład z użyciem limitu znaków:

<?php
$plik = fopen('dane.txt', 'w');

$tresc = "Przykładowa treść, którą umieścimy w pliku.";
fwrite($plik, $tresc, 30); // zapisze tylko 30 znaków

fclose($plik);

Pełny przykład zapisu:

<?php
// 1. Otwieramy plik w trybie dopisywania
$plik = fopen('log.txt', 'a') or die('Nie można otworzyć pliku');

// 2. Przygotuj dane
$data = date('Y-m-d H:i:s');
$wiadomosc = "[$data] Użytkownik zalogował się\n";

// 3. Zapisz do pliku
fwrite($plik, $wiadomosc) or die('Nie można zapisać danych');

// 4. Zamknij plik
fclose($plik);

echo "Wpis dodany do logu!";

Odczytywanie pliku

Istnieje kilka sposobów na odczytanie zawartości pliku.

fread() – odczyt określonej długości

Funkcja  czyta z pliku określoną długość znaków. Czytanie zakończy się, jeżeli osiągnięta zostanie $dlugość znaków, koniec pliku lub odczytanych zostanie 8192 bajtów. Długość całego pliku możemy określić za pomocą funkcji filesize(„nazwa_pliku.txt”)

Przykład:

<?php
$plik = fopen('dane.txt', 'r');

if ($plik === false) {
die('Nie można otworzyć pliku');
}

// Odczytaj 100 znaków
$zawartosc = fread($plik, 100);

// Lub odczytaj cały plik
// $zawartosc = fread($plik, filesize('dane.txt'));

echo $zawartosc;
fclose($plik);

fgets() – odczyt linia po linii

Innym sposobem na czytanie zawartości jest funkcja . Czyta ona jedną linię pliku, dopóki nie napotka znacznika przejścia do kolejnej linii. Żeby odczytać cały plik, wystarczy użyć pętli. W przypadku tej metody nie musimy się martwić, czy cały plik zostanie odczytany, czy przypadkiem nie przekroczyliśmy długości znaków. Funkcja feof() zwraca true, jeśli osiągnęliśmy koniec pliku. Przykład czytania pliku z użyciem fgets:

<?php
$plik = fopen('dane.txt', 'r');

if ($plik === false) {
die('Nie można otworzyć pliku');
}

echo "<pre>";

// Czytaj linię po linii
while (!feof($plik)) {
$linia = fgets($plik);
echo htmlspecialchars($linia);
}

echo "</pre>";
fclose($plik);

feof() – zwraca true, gdy osiągnięto koniec pliku.

fgetc() – odczyt znak po znaku

jest bardzo podobna w użyciu do fgets, z tą jednak różnicą, że czytamy po jednym znaku. Przykład niemal identyczny:

<?php
$plik = fopen('dane.txt', 'r');

if ($plik === false) {
die('Nie można otworzyć pliku');
}

$zawartosc = '';

while (!feof($plik)) {
$znak = fgetc($plik);
$zawartosc .= $znak;
}

echo $zawartosc;
fclose($plik);

rewind() – cofnij wskaźnik na początek

UWAGA: jeżeli kursor znajduje się na końcu pliku, to żeby odczytać dane należy przenieść kursor na początek:

<?php
$plik = fopen('dane.txt', 'r+');

// Odczytaj cały plik
$zawartosc = fread($plik, filesize('dane.txt'));

// Cofnij wskaźnik na początek
rewind($plik);

// Zapisz nowe dane
fwrite($plik, 'Nowa treść');

fclose($plik);

Blokowanie plików

Blokowanie plików ważnym zagadnieniem w kontekście operacji na plikach. Jeśli wiele osób jednocześnie wchodzi na stronę i plik jest zapisywany (np. licznik), może dojść do nadpisania danych.
Dlatego używamy blokad – flock()

Tryby blokad:

  • LOCK_SH – blokada współdzielona (tylko odczyt, wiele procesów może czytać jednocześnie)

  • LOCK_EX – blokada wyłączna (zapis, tylko jeden proces ma dostęp (zapis))

  • LOCK_UN – zwolnienie blokady

  • LOCK_NB – nie czekaj, jeśli plik jest zablokowany
<?php
$plik = fopen('licznik.txt', 'r+');

// Zablokuj plik (wyłącznie)
flock($plik, LOCK_EX);

// Odczytaj aktualną wartość
$liczba = (int)fread($plik, filesize('licznik.txt'));
$liczba++;

// Cofnij wskaźnik i zapisz nową wartość
rewind($plik);
fwrite($plik, $liczba);
fflush($plik);  // wymuś zapis na dysk

// Odblokuj plik
flock($plik, LOCK_UN);

fclose($plik);

echo "Liczba odwiedzin: $liczba";

Dlaczego flock() jest ważny:
Bez niego kilku użytkowników jednocześnie mogłoby odczytać tę samą wartość i zapisać ją, przez co licznik „zgubiłby” odwiedziny.

Ważne: Zawsze zamykaj pliki po użyciu, aby zwolnić zasoby systemowe.

Przykład: licznik odwiedzin

<?php
function incrementCounter(string $file): int {
    // Utwórz plik jeśli nie istnieje
    if (!file_exists($file)) {
        file_put_contents($file, '0');
    }
    
    $fp = fopen($file, 'r+');
    
    if ($fp === false) {
        return 0;
    }
    
    // Blokada wyłączna
    flock($fp, LOCK_EX);
    
    // Odczytaj
    $liczba = (int)fread($fp, filesize($file));
    $liczba++;
    
    // Zapisz
    rewind($fp);
    fwrite($fp, $liczba);
    fflush($fp);
    
    // Odblokuj i zamknij
    flock($fp, LOCK_UN);
    fclose($fp);
    
    return $liczba;
}

// Użycie
$odwiedziny = incrementCounter('licznik.txt');
echo "Odwiedziny: $odwiedziny";

unlink() – usuwanie pliku

Przykład:

<?php
$plik = 'do_usuniecia.txt';

if (file_exists($plik)) {
if (unlink($plik)) {
echo "Plik usunięty";
} else {
echo "Błąd usuwania";
}
} else {
echo "Plik nie istnieje";
}

Nie każdy plik może zostać usunięty, wszystko zależy od odpowiednich praw dostępu.

Uwaga: Plik jest usuwany permanentnie (nie trafia do kosza).

copy() – kopiowanie pliku

Pozwala utworzyć duplikat pliku w nowej lokalizacji.

Przykład:

<?php
$zrodlo = 'dane.txt';
$cel = 'kopia_dane.txt';

if (copy($zrodlo, $cel)) {
echo "Plik skopiowany";
} else {
echo "Błąd kopiowania";
}

// Kopiowanie do innego katalogu
copy('dane.txt', 'backup/dane.txt');

Uwaga: Jeśli plik docelowy istnieje, zostanie nadpisany.

rename() – zmiana nazwy / przenoszenie

Ta funkcja zmienia nazwę pliku lub przenosi go do innego katalogu.

Przykład:

<?php
// Zmiana nazwy
rename('stara_nazwa.txt', 'nowa_nazwa.txt');

// Przeniesienie do innego katalogu
rename('plik.txt', 'archiwum/plik.txt');

// Przeniesienie + zmiana nazwy
rename('dane.txt', 'backup/dane_2025.txt');

// Działa też dla katalogów
rename('stary_folder', 'nowy_folder');

Działa także dla katalogów.

filesize() – rozmiar pliku

<?php
$plik = 'dokument.pdf';

if (file_exists($plik)) {
    $rozmiar = filesize($plik);
    echo "Rozmiar: $rozmiar bajtów\n";
    
    // Sformatowany
    echo "Rozmiar: " . formatBytes($rozmiar);
}

function formatBytes(int $bytes, int $precision = 2): string {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    
    for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
        $bytes /= 1024;
    }
    
    return round($bytes, $precision) . ' ' . $units[$i];
}

filemtime() – data ostatniej modyfikacji

<?php
$plik = 'dane.txt';

if (file_exists($plik)) {
    $timestamp = filemtime($plik);
    
    echo "Ostatnia modyfikacja: " . date('Y-m-d H:i:s', $timestamp);
    
    // Sprawdź wiek pliku
    $wiek = time() - $timestamp;
    $dni = floor($wiek / (60 * 60 * 24));
    
    echo "\nPlik ma $dni dni";
}

Alternatywne podejście

Trzeba pamiętać, że funkcja fopen() daje większą kontrolę i lepiej sprawdza się w „poważniejszych” zastosowaniach (duże dane, blokady, odczyt/zapis równocześnie). Istnieje dla niej prostsza alternatywa.

file_put_contents() – prostszy zapis

W PHP najprostszy i najszybszy sposób zapisu danych do pliku to użycie funkcji file_put_contents():

  • jedna linijka kodu
  • automatyczne otwarcie i zamknięcie
  • szybkie i czytelne
<?php
// Podstawowy zapis (nadpisuje plik)
$dane = "To jest przykładowy tekst.\n";
file_put_contents('plik.txt', $dane);

// Dopisywanie na końcu
file_put_contents('log.txt', $dane, FILE_APPEND);

// Zapis tablicy
$tablica = ['Jan', 'Anna', 'Piotr'];
file_put_contents('lista.txt', implode("\n", $tablica));

// Sprawdź czy się udało
$wynik = file_put_contents('dane.txt', 'treść');
if ($wynik !== false) {
    echo "Zapisano $wynik bajtów";
}
  • tworzy plik plik.txt (jeśli nie istnieje),
  • zapisuje w nim zawartość zmiennej $dane,
  • jeśli plik już istnieje — nadpisze go.

Dopisywanie do istniejącego pliku (zamiast nadpisywania):

Użycie flagi FILE_APPEND:

<?php
$dane = "Nowa linia\n";
file_put_contents("plik.txt", $dane, FILE_APPEND);
?>

Wtedy kolejne wywołania dopisują treść na końcu pliku.

file_get_contents() – prostszy odczyt

<?php
// Odczyt całego pliku
$zawartosc = file_get_contents('dane.txt');
echo $zawartosc;

// Z zewnętrznego URL
$html = file_get_contents('https://example.com');

// Sprawdź błędy
$zawartosc = file_get_contents('dane.txt');
if ($zawartosc === false) {
    echo "Błąd odczytu";
}

file() – odczyt do tablicy

Każda linia pliku staje się elementem tablicy.

<?php
// Odczyt linii do tablicy
$linie = file('dane.txt');

foreach ($linie as $nr => $linia) {
    echo "Linia " . ($nr + 1) . ": " . $linia;
}

// Bez pustych linii i \n
$linie = file('dane.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

print_r($linie);

Flagi:

  • FILE_IGNORE_NEW_LINES – usuwa \n z końca linii
  • FILE_SKIP_EMPTY_LINES – pomija puste linie

Porównanie metod

<?php
// ============================================
// TRADYCYJNA METODA (3 kroki)
// ============================================
$plik = fopen('dane.txt', 'w');
fwrite($plik, 'Treść');
fclose($plik);

// ============================================
// NOWOCZESNA METODA (1 linijka)
// ============================================
file_put_contents('dane.txt', 'Treść');


// ============================================
// ODCZYT - tradycyjny
// ============================================
$plik = fopen('dane.txt', 'r');
$zawartosc = fread($plik, filesize('dane.txt'));
fclose($plik);

// ============================================
// ODCZYT - nowoczesny
// ============================================
$zawartosc = file_get_contents('dane.txt');

Przykłady praktyczne

1. System logowania

<?php
function logEvent(string $message, string $level = 'INFO'): void {
    $logFile = 'app.log';
    $timestamp = date('Y-m-d H:i:s');
    $logEntry = "[$timestamp] [$level] $message\n";
    
    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

// Użycie
logEvent('Użytkownik zalogowany', 'INFO');
logEvent('Błąd połączenia z bazą', 'ERROR');
logEvent('Ostrzeżenie: mało miejsca', 'WARNING');

2. Cache systemu

<?php
function cache(string $key, callable $callback, int $ttl = 3600): mixed {
    $cacheFile = "cache/$key.cache";
    
    // Sprawdź czy cache jest aktualny
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
        return unserialize(file_get_contents($cacheFile));
    }
    
    // Wygeneruj nowe dane
    $data = $callback();
    
    // Zapisz w cache
    if (!is_dir('cache')) {
        mkdir('cache', 0755, true);
    }
    file_put_contents($cacheFile, serialize($data), LOCK_EX);
    
    return $data;
}

// Użycie
$users = cache('users', function() {
    // Symulacja wolnego zapytania do bazy
    sleep(2);
    return ['Jan', 'Anna', 'Piotr'];
}, 60);  // cache na 60 sekund

print_r($users);

3. Prosty licznik odwiedzin

<?php
function getVisitCount(string $file = 'counter.txt'): int {
    if (!file_exists($file)) {
        file_put_contents($file, '0');
    }
    
    $fp = fopen($file, 'r+');
    flock($fp, LOCK_EX);
    
    $count = (int)fgets($fp);
    $count++;
    
    rewind($fp);
    fwrite($fp, $count);
    fflush($fp);
    
    flock($fp, LOCK_UN);
    fclose($fp);
    
    return $count;
}

$visits = getVisitCount();
echo "Odwiedziny: $visits";

4. CSV reader/writer

<?php
class CSVHandler {
    private string $file;
    
    public function __construct(string $file) {
        $this->file = $file;
    }
    
    public function read(): array {
        if (!file_exists($this->file)) {
            return [];
        }
        
        $data = [];
        $handle = fopen($this->file, 'r');
        
        // Pomijamy nagłówek
        fgetcsv($handle);
        
        while (($row = fgetcsv($handle)) !== false) {
            $data[] = $row;
        }
        
        fclose($handle);
        return $data;
    }
    
    public function write(array $data, array $header = []): void {
        $handle = fopen($this->file, 'w');
        
        // Zapisz nagłówek
        if (!empty($header)) {
            fputcsv($handle, $header);
        }
        
        // Zapisz dane
        foreach ($data as $row) {
            fputcsv($handle, $row);
        }
        
        fclose($handle);
    }
    
    public function append(array $row): void {
        $handle = fopen($this->file, 'a');
        fputcsv($handle, $row);
        fclose($handle);
    }
}

// Użycie
$csv = new CSVHandler('users.csv');

// Zapis
$csv->write([
    ['Jan', 'Kowalski', 30],
    ['Anna', 'Nowak', 25]
], ['Imię', 'Nazwisko', 'Wiek']);

// Odczyt
$users = $csv->read();
print_r($users);

// Dopisz
$csv->append(['Piotr', 'Wiśniewski', 35]);

5. Prosty szablon z plikami

<?php
function renderTemplate(string $template, array $vars = []): string {
    $file = "templates/$template.html";
    
    if (!file_exists($file)) {
        throw new Exception("Template not found: $template");
    }
    
    $content = file_get_contents($file);
    
    // Zamień zmienne {{zmienna}}
    foreach ($vars as $key => $value) {
        $content = str_replace("{{" . $key . "}}", $value, $content);
    }
    
    return $content;
}

// templates/email.html:
// <h1>Witaj {{name}}!</h1>
// <p>Twoje konto zostało utworzone. Twój email to: {{email}}</p>

// Użycie
echo renderTemplate('email', [
    'name' => 'Jan',
    'email' => 'jan@example.com'
]);

6. Zapis/odczyt danych z formularza do pliku i usunięcie pliku

index.php:

<?php
// ============================================
// FORMULARZ Z ZAPISEM DO PLIKU
// ============================================

if (empty($_POST['pole1']) && empty($_POST['pole2'])) {
    // Wyświetl formularz
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <title>Formularz</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                max-width: 600px;
                margin: 50px auto;
                padding: 20px;
            }
            input, textarea {
                width: 100%;
                padding: 10px;
                margin: 10px 0;
                box-sizing: border-box;
            }
            button {
                background: #007bff;
                color: white;
                padding: 10px 20px;
                border: none;
                cursor: pointer;
            }
            button:hover {
                background: #0056b3;
            }
        </style>
    </head>
    <body>
        <h2>Formularz zapisu danych</h2>
        <form method="POST">
            <label>Tytuł:</label>
            <input type="text" name="pole1" required>
            
            <label>Treść:</label>
            <textarea name="pole2" rows="5" required></textarea>
            
            <button type="submit">Zapisz</button>
        </form>
    </body>
    </html>
    <?php
} else {
    // Przetwórz formularz
    $pole1 = trim($_POST['pole1']);
    $pole2 = trim($_POST['pole2']);
    
    // Walidacja
    if (empty($pole1) || empty($pole2)) {
        die('Wszystkie pola są wymagane!');
    }
    
    // Zabezpieczenia
    $pole1 = htmlspecialchars($pole1, ENT_QUOTES, 'UTF-8');
    $pole2 = htmlspecialchars($pole2, ENT_QUOTES, 'UTF-8');
    
    // Przygotuj dane (separator `)
    $dane = $pole1 . "`" . $pole2 . "\n";
    
    // Zapisz z blokowaniem
    $file = "baza.txt";
    $fp = fopen($file, "a");
    
    if ($fp === false) {
        die('Błąd otwarcia pliku');
    }
    
    flock($fp, LOCK_EX);        // blokada
    fwrite($fp, $dane);          // zapis
    fflush($fp);                 // wymuś zapis
    flock($fp, LOCK_UN);         // odblokuj
    fclose($fp);
    
    echo '<!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <title>Sukces</title>
        <style>
            body { font-family: Arial; max-width: 600px; margin: 50px auto; }
            .success { background: #d4edda; padding: 15px; border: 1px solid #c3e6cb; }
            a { color: #007bff; }
        </style>
    </head>
    <body>
        <div class="success">
            <h2>Dane zostały zapisane!</h2>
        </div>
        <p><a href="podglad.php">Zobacz wszystkie wpisy</a></p>
        <p><a href="index.php">Dodaj kolejny wpis</a></p>
    </body>
    </html>';
}

podglad.php:

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Podgląd wpisów</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        .wpis {
            background: #f8f9fa;
            padding: 15px;
            margin: 15px 0;
            border-left: 4px solid #007bff;
        }
        .tytul {
            font-size: 1.2em;
            font-weight: bold;
            color: #007bff;
            margin-bottom: 10px;
        }
        .tresc {
            color: #333;
            line-height: 1.6;
        }
        .nav {
            margin: 20px 0;
        }
        a {
            display: inline-block;
            margin-right: 10px;
            padding: 10px 15px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 4px;
        }
        a:hover {
            background: #0056b3;
        }
        .danger {
            background: #dc3545;
        }
        .danger:hover {
            background: #c82333;
        }
    </style>
</head>
<body>
    <h2>📝 Wszystkie wpisy</h2>
    
    <?php
    if (!file_exists('baza.txt') || filesize('baza.txt') == 0) {
        echo '<p>Brak wpisów w bazie.</p>';
    } else {
        $file = file('baza.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        
        foreach ($file as $nr => $value) {
            // Rozdziel po znaku `
            $exp = explode('`', $value);
            
            if (count($exp) >= 2) {
                echo '<div class="wpis">';
                echo '<div class="tytul">' . ($nr + 1) . '. ' . $exp[0] . '</div>';
                echo '<div class="tresc">' . nl2br($exp[1]) . '</div>';
                echo '</div>';
            }
        }
    }
    ?>
    
    <div class="nav">
        <a href="index.php">Dodaj kolejne dane</a>
        <a href="del.php" class="danger" onclick="return confirm('Czy na pewno usunąć wszystkie dane?')">Usuń plik</a>
    </div>
</body>
</html>

del.php:

<?php
// Usuń plik
if (file_exists('baza.txt')) {
    if (unlink('baza.txt')) {
        $message = 'Plik został usunięty';
        $class = 'success';
    } else {
        $message = 'Błąd podczas usuwania pliku';
        $class = 'error';
    }
} else {
    $message = 'Plik nie istnieje';
    $class = 'warning';
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Usuwanie pliku</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        .message {
            padding: 15px;
            margin: 20px 0;
            border-radius: 4px;
        }
        .success {
            background: #d4edda;
            border: 1px solid #c3e6cb;
            color: #155724;
        }
        .error {
            background: #f8d7da;
            border: 1px solid #f5c6cb;
            color: #721c24;
        }
        .warning {
            background: #fff3cd;
            border: 1px solid #ffeeba;
            color: #856404;
        }
        a {
            display: inline-block;
            margin-top: 20px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 4px;
        }
        a:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>
    <div class="message <?= $class ?>">
        <h2><?= $message ?></h2>
    </div>
    
    <a href="index.php">Powrót do formularza</a>
</body>
</html>

Tabela funkcji – szybka ściąga

KategoriaFunkcjaOpis
Sprawdzanie
file_exists()Czy plik/katalog istnieje
is_file()Czy to plik
is_readable()Czy można odczytać
is_writable()Czy można zapisać
Otwieranie/Zamykanie
fopen()Otwórz plik
fclose()Zamknij plik
Zapis tradycyjny
fwrite() / fputs()Zapisz dane
fflush()Wymuś zapis na dysk
Zapis nowoczesny
file_put_contents()Zapisz (1 linijka)
Odczyt tradycyjny
fread()Odczytaj N bajtów
fgets()Odczytaj linię
fgetc()Odczytaj znak
feof()Koniec pliku?
Odczyt nowoczesny
file_get_contents()Odczytaj cały plik
file()Odczytaj do tablicy
Blokowanie
flock()Zablokuj/odblokuj
Operacje
unlink()Usuń plik
copy()Kopiuj plik
rename()Zmień nazwę/przenieś
Informacje
filesize()Rozmiar pliku
filemtime()Data modyfikacji
Wskaźnik
rewind()Cofnij na początek

Dobre praktyki

1. Zawsze sprawdzaj wynik operacji

// ŹLE
$plik = fopen('dane.txt', 'r');
$zawartosc = fread($plik, 100);

// DOBRZE
$plik = fopen('dane.txt', 'r');
if ($plik === false) {
    die('Błąd otwarcia pliku');
}
$zawartosc = fread($plik, filesize('dane.txt'));
fclose($plik);

2. Używaj file_put_contents() dla prostych operacji

// ŹLE - zbyt skomplikowane
$plik = fopen('log.txt', 'a');
fwrite($plik, 'wpis');
fclose($plik);

// DOBRZE - prosty i czytelny
file_put_contents('log.txt', 'wpis', FILE_APPEND);

3. Zawsze używaj flock() dla współbieżności

// ŹLE - ryzyko utraty danych
$fp = fopen('licznik.txt', 'r+');
$liczba = fgets($fp);
$liczba++;
rewind($fp);
fwrite($fp, $liczba);
fclose($fp);

// DOBRZE - bezpieczne
$fp = fopen('licznik.txt', 'r+');
flock($fp, LOCK_EX);
$liczba = fgets($fp);
$liczba++;
rewind($fp);
fwrite($fp, $liczba);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);

4. Waliduj dane przed zapisem

// ŹLE
$dane = $_POST['dane'];
file_put_contents('plik.txt', $dane);

// DOBRZE
$dane = trim($_POST['dane']);
$dane = htmlspecialchars($dane, ENT_QUOTES, 'UTF-8');

if (empty($dane)) {
    die('Dane nie mogą być puste');
}

file_put_contents('plik.txt', $dane);

5. Używaj try-catch dla krytycznych operacji

<?php
try {
    if (!file_exists('config.ini')) {
        throw new Exception('Plik konfiguracyjny nie istnieje');
    }
    
    $config = file_get_contents('config.ini');
    
    if ($config === false) {
        throw new Exception('Błąd odczytu pliku');
    }
    
    // ... przetwarzanie ...
    
} catch (Exception $e) {
    error_log($e->getMessage());
    die('Błąd aplikacji');
}

Podsumowanie

Kluczowe pojęcia:

  • fopen/fclose – tradycyjne otwieranie/zamykanie plików
  • fread/fgets/fgetc – różne metody odczytu
  • fwrite/fputs – zapis danych
  • flock – blokowanie dla współbieżności
  • file_put_contents/file_get_contents – nowoczesne, szybkie metody

Tryby otwierania:

  • r – odczyt (plik musi istnieć)
  • w – zapis (nadpisuje/tworzy)
  • a – dopisywanie (na końcu)
  • x – tworzenie (błąd jeśli istnieje)
  • + – odczyt i zapis

Pamiętaj:

  • Zawsze zamykaj pliki fclose()
  • Używaj flock() dla współbieżnego dostępu
  • Preferuj file_put_contents() dla prostych operacji
  • Sprawdzaj wynik operacji (może być false)
  • Waliduj dane przed zapisem
  • Używaj FILE_APPEND do dopisywania
  • Nie ufaj danym użytkownika
  • Nie zapomnij o fflush() przy blokadach

Najważniejsze funkcje:

// Proste operacje
file_put_contents()  // zapis
file_get_contents()  // odczyt
file()              // odczyt do tablicy

// Zaawansowane
fopen()    // otwórz
fwrite()   // zapisz
fread()    // czytaj
flock()    // zablokuj
fclose()   // zamknij

// Pomocnicze
file_exists()  // czy istnieje
filesize()     // rozmiar
filemtime()    // data modyfikacji
copy()         // kopiuj
rename()       // zmień nazwę
unlink()       // usuń

Zadania do wykonania:

Zadanie 1

Generowanie i analiza losowych liczb z zapisem do pliku.

Wymagania:

  1. Wygeneruj 20 losowych liczb (1-100)
  2. Zapisz do liczby.txt z blokowaniem
  3. Odczytaj i oblicz:
    • sumę
    • średnią
    • min i max
    • liczby parzyste/nieparzyste
  4. Zapisz statystyki do statystyki.txt

Zadanie 2

Losowanie liczb w stylu Lotto z zapisem.

Wymagania:

  1. Wygeneruj 6 unikalnych liczb (1-49)
  2. Pobierz aktualną datę
  3. Zapisz do liczby.txt w formacie: 2025-10-13 15:20:00 wylosowano liczby: 3, 15, 22, 31, 42, 45
  4. Odczytaj i wyświetl zawartość

TaskQuest v1.1 — dane żyją poza kodem

KARTA PRACY

Po Lekcji 10 TaskQuest ma porządek w kodzie, ale lista zadań nadal jest częścią programu — leży w data/zadania.php jako tablica PHP. Żeby zmienić termin jednego zadania, trzeba edytować kod. Jeden zgubiony przecinek i cała aplikacja przestaje działać.

Dziś oddzielamy dane od programu: zadania trafią do zwykłego pliku tekstowego, który TaskQuest odczytuje przy każdym uruchomieniu.

Cel lekcji

Nauczyć TaskQuest:

  • otwierać, czytać, zapisywać i zamykać pliki (fopen(), fgets(), fwrite(), fclose()) oraz rozumieć tryby r, w, a,
  • sprawdzać, czy plik da się odczytać (is_file(), is_readable()) i reagować, gdy się nie da,
  • zapisywać i odczytywać dane tabelaryczne w formacie CSV (fputcsv(), fgetcsv()),
  • zamieniać tekst z pliku z powrotem na dane o właściwych typach (int, bool).

Efekt: data/zadania.php znika, a jego miejsce zajmuje data/zadania.csv. Funkcje z Lekcji 9 (pobierzAktywneZadania(), sortujPoTerminie() itd.) działają dalej bez żadnej zmiany, bo $zadania ma dokładnie tę samą strukturę.

Uwaga — co jest, a czego jeszcze nie ma: po tej lekcji TaskQuest odczytuje zadania z pliku i potrafi je zapisać (funkcja zapiszZadania()), ale użytkownik nie ma jeszcze jak tego zrobić z poziomu strony — zapis wywołujemy z kodu (migracja danych, testy). Formularz dodawania zadania pojawi się w Lekcji 13. Dane grywalizacji ($xp, $streak, $dataOstatniejAktywnosci) nadal są zapisane na sztywno w index.php — to nadal symulacja.

Rozgrzewka — 4 pytania z Lekcji 10

Odpowiedz z pamięci, zanim zaczniesz:

  1. Czym różni się require od require_once i którego używamy dla plików z funkcjami?
  2. Co przechowuje __DIR__ w pliku includes/header.php?
  3. Co zwraca scandir() oprócz nazw plików i jak radzi sobie z tym listaPlikowWKatalogu()?
  4. Dlaczego w plikach zawierających wyłącznie PHP nie zamykamy tagu ?>?

1. Wracamy do naszego TaskQuest

Mamy aplikację wieloplikową: konfigurację, funkcje w osobnych plikach, szkielet strony, dane zadań i stronę diagnostyczną. Wszystkie zmienne i funkcje z Lekcji 10 zostają.

Docelowa struktura projektu

taskquest/
├── index.php                        (zmiana: zadania z pliku)
├── diagnostyka.php                  (zmiana: nowe pliki na liście)
├── migracja.php                     (nowe — jednorazowe, na końcu lekcji usuniemy)
├── config/
│   └── config.php                   (zmiana: dochodzi PLIK_ZADAN)
├── includes/
│   ├── header.php
│   ├── footer.php                   (zmiana: "TaskQuest 1.1")
│   ├── funkcje_grywalizacji.php
│   ├── funkcje_tekstowe.php
│   ├── funkcje_terminow.php
│   ├── funkcje_zadan.php
│   ├── funkcje_diagnostyczne.php
│   └── funkcje_plikow.php           (nowe)
└── data/
    └── zadania.csv                  (nowe — zastępuje zadania.php)

Co gdzie trafi

PlikZawartość
config/config.phpstała PLIK_ZADAN — ścieżka do pliku z zadaniami
includes/funkcje_plikow.phpwczytajZadania(), zapiszZadania()
data/zadania.csvzadania jako zwykły tekst: jedno zadanie = jedna linia
migracja.phpjednorazowe przepisanie starej tablicy do CSV

Przygotowanie (zrób to teraz)

  1. Zrób kopię zapasową całego katalogu taskquest/ z Lekcji 10 — np. jako taskquest_lekcja10/ (obok, nie w środku). Przyda się do testu regresji.
  2. Uruchom index.php i zapamiętaj (lub zrób zrzut), jak wygląda strona.

2. Problem w aplikacji

Kama chce dodać do TaskQuest nowe zadanie. Otwiera data/zadania.php, dopisuje blok w nawiasach kwadratowych, zapomina przecinka — i cała strona wyświetla Parse error. Zmieniła jedno zadanie, a zepsuła cały program.

Jest też problem na przyszłość: gdy pojawią się formularze (Lekcja 13), aplikacja będzie musiała dopisywać zadania sama. Program, który przepisuje własny kod, to bardzo zły pomysł.

Potrzebujemy:

  1. miejsca na dane, które nie jest kodem — pliku, który można otworzyć w edytorze i poprawić,
  2. sposobu, żeby PHP odczytało ten plik i zamieniło go z powrotem na tablicę $zadania,
  3. sposobu, żeby PHP potrafiło ten plik zapisać.

3. Nowy mechanizm PHP: praca z plikami

3.1 Trzy kroki: otwórz → działaj → zamknij

Każda praca z plikiem wygląda tak samo:

fopen()  →  czytanie lub pisanie  →  fclose()

fopen() zwraca uchwyt — coś w rodzaju zakładki w otwartym pliku. Wszystkie kolejne funkcje dostają ten uchwyt i wiedzą, na którym pliku mają pracować.

Trzeci argument — tryb — mówi, co zamierzasz robić:

TrybCo robiGdy pliku nie maUwaga
rtylko odczytfopen() zwraca falseplik nietknięty
wzapis od zeraplik jest tworzonyod razu kasuje całą dotychczasową zawartość
adopisywanie na końcuplik jest tworzonydotychczasowa zawartość zostaje

Utwórz w katalogu taskquest/ plik brudnopisu brudnopis.php.

Zapis:

<?php

$uchwyt = fopen(__DIR__ . '/dziennik.txt', 'w');
fwrite($uchwyt, "Start TaskQuest\n");
fclose($uchwyt);

Uruchom, a potem otwórz dziennik.txt w edytorze. Jest w nim jedna linia. \n to znak nowej linii — bez niego kolejne zapisy zlepiłyby się w jedną linię.

Odczyt (zastąp kod w brudnopis.php):

<?php

$uchwyt = fopen(__DIR__ . '/dziennik.txt', 'r');

while (($linia = fgets($uchwyt)) !== false) {
    echo trim($linia) . "<br>";
}

fclose($uchwyt);

Co tu się dzieje?

  • fgets($uchwyt) czyta jedną linię i przesuwa „zakładkę” dalej. Gdy plik się skończy, zwraca false.
  • W nawiasie jest przypisanie i porównanie naraz: najpierw $linia = fgets(...), potem wynik jest porównywany z false.
  • Dlaczego !== false, a nie samo while ($linia = fgets(...))? Bo ostatnia linia pliku może mieć treść "0", a "0" w warunku jest traktowane jak false — pętla zatrzymałaby się o linię za wcześnie. !== false jest odporne na taki przypadek.
  • fgets() zostawia na końcu linii znak \n, dlatego czyścimy go przez trim().

3.2 Plik może się nie otworzyć

fopen() nie zawsze się udaje: plik może nie istnieć, może brakować uprawnień, ścieżka może być błędna. Wtedy funkcja zwraca false (i wyświetla ostrzeżenie), a dalsze fgets() na false to już zupełnie inny, gorszy błąd.

Dlatego zawsze:

  1. przed otwarciem sprawdzamy: is_file() (z Lekcji 10 — czy to plik) oraz is_readable() (czy PHP ma prawo go czytać),
  2. po otwarciu sprawdzamy: $uchwyt === false.
if (!is_file($sciezka) || !is_readable($sciezka)) {
    // nie ma czego czytać
}

Na tym etapie „reagowanie” oznacza po prostu zwrócenie pustego wyniku. Prawdziwą obsługę błędów (try/catch) poznamy w Lekcji 12 — i wrócimy wtedy do tych funkcji.

3.3 Skróty dla prostych przypadków

PHP ma też funkcje, które robią wszystko jednym poleceniem:

FunkcjaCo robi
file_put_contents($plik, $tekst)zapisuje cały tekst (nadpisuje plik)
file_put_contents($plik, $tekst, FILE_APPEND)dopisuje tekst na końcu
file_get_contents($plik)zwraca całą zawartość jako jeden tekst
file($plik)zwraca zawartość jako tablicę linii
unlink($plik)usuwa plik

Są wygodne przy krótkich tekstach (np. w testach). Dane TaskQuest odczytujemy jednak linia po linii i „rozbieramy” na pola, więc główną pracę wykonają fopen() i spółka.

3.4 CSV — tabela w pliku tekstowym

CSV to zwykły plik tekstowy, w którym jedna linia to jeden rekord, a pola oddzielone są separatorem. My użyjemy średnika ; (tak robi polski Excel, a przecinek bywa w tekstach).

PHP ma do tego dwie funkcje:

  • fputcsv($uchwyt, $tablica, ';', '"', '') — zamienia tablicę w jedną linię CSV i zapisuje ją,
  • fgetcsv($uchwyt, 0, ';', '"', '') — czyta jedną linię CSV i zwraca ją jako tablicę pól (albo false na końcu pliku).

Wypróbuj w brudnopis.php:

<?php

$uchwyt = fopen(__DIR__ . '/gracze.csv', 'w');
fputcsv($uchwyt, ['login', 'xp'], ';', '"', '');
fputcsv($uchwyt, ['Kama_2026', 250], ';', '"', '');
fputcsv($uchwyt, ['Ola Nowak', 120], ';', '"', '');
fclose($uchwyt);

$uchwyt = fopen(__DIR__ . '/gracze.csv', 'r');

echo "<pre>";
while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
    print_r($wiersz);
}
echo "</pre>";

fclose($uchwyt);

Otwórz gracze.csv w edytorze. Zobaczysz:

login;xp
Kama_2026;250
"Ola Nowak";120

Zwróć uwagę na cudzysłowy: fputcsv() sam otacza nimi pola, które zawierają spacje, separator albo cudzysłów. Dzięki temu tytuł zadania w rodzaju Ćwiczenia; DateTime nie rozbije się na dwa pola.

Po co te dwa ostatnie parametry ('"', '')? To ogranicznik tekstu (cudzysłów) i tzw. znak ucieczki. Pusty znak ucieczki wyłącza stary, nietypowy mechanizm oparty na ukośniku \. Od PHP 8.4 samo pominięcie tego parametru wywołuje komunikat Deprecated, więc podajemy go zawsze — najprościej skopiować całą końcówkę ';', '"', ''. Drugi parametr fgetcsv() (0) oznacza „bez limitu długości linii”.

3.5 Z pliku wraca wyłącznie tekst

Zauważ, co zwraca fgetcsv(): każde pole jest tekstem (string). Liczba 250 wraca jako "250", a true zapisane jako 1 wraca jako "1".

var_dump("1" == 1);    // true  — luźne porównanie
var_dump("1" === 1);   // false — inny typ

Nasze funkcje z Lekcji 9 oczekują liczb i wartości logicznych, więc przy odczycie musimy je odtworzyć:

Pole w plikuJak zamieniamy
liczba (id, xp)(int) $wiersz[0]
wartość logiczna (wykonane)$wiersz[4] === '1'

Dlaczego nie (bool) $wiersz[4]? Bo (bool) "0" daje false, ale (bool) "nie" daje true — każdy niepusty tekst poza "0" jest „prawdą”. Porównanie z '1' jest jednoznaczne.

Typowe błędy przy plikach

  • Zapis w trybie w do jedynej kopii ważnego pliku — dane znikają w momencie otwarcia, nawet jeśli potem nic nie zapiszesz.
  • Brak sprawdzenia === false po fopen().
  • Edycja CSV w Excelu: program potrafi po zapisie zmienić format dat (2026-09-20 na 20.09.2026), separator i kodowanie. Pliki .csv w tej lekcji edytujemy w edytorze kodu (zapis w UTF-8).
  • Ścieżka względna zamiast __DIR__ — ten sam problem co w Lekcji 10.
  • Zapomniane fclose().

4. SAMODZIELNIE — dziennik.txt (tryby w i a)

W nowym pliku brudnopis.php (zastąp poprzednią zawartość) napisz kod, który:

  1. otworzy dziennik.txt w trybie w i zapisze linię Start TaskQuest,
  2. otworzy ten sam plik w trybie a i dopisze dwie linie: Wczytano zadania oraz Koniec,
  3. otworzy plik w trybie r i wypisze każdą linię z numerem.

Efekt w przeglądarce:

1. Start TaskQuest
2. Wczytano zadania
3. Koniec

Jak sprawdzić: otwórz dziennik.txt w edytorze — mają być trzy linie. Odśwież stronę kilka razy: wynik nie powinien się zmieniać. Pomyśl, dlaczego.

HINT

Trzy razy powtarzasz ten sam schemat: fopen() → praca → fclose(). Zmienia się tylko tryb. Numer linii to zwykły licznik zwiększany w pętli.

POMOC
$plik = __DIR__ . '/dziennik.txt';

$uchwyt = fopen($plik, '...');
fwrite($uchwyt, "...\n");
fclose($uchwyt);

// ... tryb a ...

// ... tryb r + while + fgets ...
ROZWIĄZANIE
<?php

$plik = __DIR__ . '/dziennik.txt';

$uchwyt = fopen($plik, 'w');
fwrite($uchwyt, "Start TaskQuest\n");
fclose($uchwyt);

$uchwyt = fopen($plik, 'a');
fwrite($uchwyt, "Wczytano zadania\n");
fwrite($uchwyt, "Koniec\n");
fclose($uchwyt);

$uchwyt = fopen($plik, 'r');
$numer = 1;

while (($linia = fgets($uchwyt)) !== false) {
    echo $numer . ". " . trim($linia) . "<br>";
    $numer++;
}

fclose($uchwyt);

Odświeżanie nie zmienia wyniku, bo każde uruchomienie zaczyna od trybu w, który czyści plik.

5. SAMODZIELNIE — odznaki.csv (CSV i typy)

W nowym pliku brudnopis.php napisz kod, który:

  1. zapisze do odznaki.csv nagłówek id;nazwa;poziom i trzy odznaki:
    1 / Pierwszy krok / 1, 2 / Mistrz; regexów / 2, 3 / Legenda TaskQuest / 5,
  2. odczyta plik (pomijając nagłówek) i wypisze każdą odznakę w formie Nazwa — wymagany poziom N,
  3. policzy, ile odznak wymaga poziomu 2 lub wyższego, i wypisze wynik.

Efekt w przeglądarce:

Pierwszy krok — wymagany poziom 1
Mistrz; regexów — wymagany poziom 2
Legenda TaskQuest — wymagany poziom 5
Odznak od poziomu 2 wzwyż: 2

Jak sprawdzić: otwórz odznaki.csv. Nazwa ze średnikiem musi być w cudzysłowie, a mimo to w przeglądarce ma się wyświetlić jako jedno pole.

HINT

Nagłówek pomijasz jednym dodatkowym wywołaniem fgetcsv() przed pętlą. Poziom z pliku jest tekstem — przed porównaniem z liczbą zamień go na int.

POMOC
$odznaki = [
    [1, 'Pierwszy krok', 1],
    // ...
];

foreach ($odznaki as $odznaka) {
    fputcsv(...);
}

// przy odczycie:
if ((int) $wiersz[2] >= 2) { ... }
ROZWIĄZANIE
<?php

$plik = __DIR__ . '/odznaki.csv';

$odznaki = [
    [1, 'Pierwszy krok', 1],
    [2, 'Mistrz; regexów', 2],
    [3, 'Legenda TaskQuest', 5]
];

$uchwyt = fopen($plik, 'w');
fputcsv($uchwyt, ['id', 'nazwa', 'poziom'], ';', '"', '');

foreach ($odznaki as $odznaka) {
    fputcsv($uchwyt, $odznaka, ';', '"', '');
}

fclose($uchwyt);

$uchwyt = fopen($plik, 'r');
fgetcsv($uchwyt, 0, ';', '"', '');   // pomijamy nagłówek
$ileWyzszych = 0;

while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
    echo $wiersz[1] . " — wymagany poziom " . $wiersz[2] . "<br>";

    if ((int) $wiersz[2] >= 2) {
        $ileWyzszych++;
    }
}

fclose($uchwyt);

echo "Odznak od poziomu 2 wzwyż: $ileWyzszych";

6. Przenosimy zadania do pliku — krok po kroku

Mamy już wszystkie narzędzia. Kolejność jest ważna: najpierw nauczymy TaskQuest zapisywać zadania (dzięki temu nie przepisujemy pięciu zadań ręcznie), a potem odczytywać.

Krok 1 — ścieżka do pliku w konfiguracji

W config/config.php, pod define('XP_NA_POZIOM', 100);, dodaj:

define('PLIK_ZADAN', dirname(__DIR__) . '/data/zadania.csv');

__DIR__ w tym pliku to katalog config/, a dirname() (z Lekcji 10) daje katalog wyżej — główny katalog projektu. Ścieżkę zapisujemy w jednym miejscu, tak jak próg XP: zmiana nazwy pliku dotyczy jednej linii.

Krok 2 — format pliku

Plik data/zadania.csv będzie miał nagłówek (żeby człowiek wiedział, co jest w której kolumnie) i po jednej linii na zadanie:

KolumnaKlucz w $zadaniaTypJak odtwarzamy
id'id'int(int)
tytul'tytul'stringbez zmian (surowy — czyści go wyczyscTytul() przy wyświetlaniu)
opis'opis'stringbez zmian
termin'termin'string (RRRR-MM-DD)bez zmian
wykonane'wykonane'bool=== '1' (w pliku 1 lub 0)
xp'xp'int(int)

Kolejność kolumn w pliku jest umową — zapis i odczyt muszą się z nią zgadzać.

Krok 3 — SAMODZIELNIE: zapiszZadania()

Utwórz plik includes/funkcje_plikow.php (pierwsza linia <?php, bez zamykającego ?>) i napisz w nim funkcję:

function zapiszZadania(string $sciezka, array $zadania): bool

Funkcja ma:

  1. otworzyć plik w trybie w; jeśli się nie da — zwrócić false,
  2. zapisać wiersz nagłówka id;tytul;opis;termin;wykonane;xp,
  3. zapisać każde zadanie z $zadania jako jeden wiersz (w kolejności kolumn z Kroku 2; wykonane jako 1 lub 0),
  4. zamknąć plik i zwrócić true.
HINT

Przechodzisz po $zadania pętlą foreach — dokładnie tak jak w sumujXp(). Wartość logiczną zamienisz na 1/0 operatorem warunkowym ? :.

POMOC
function zapiszZadania(string $sciezka, array $zadania): bool
{
    $uchwyt = fopen($sciezka, '...');

    if ($uchwyt === false) {
        return false;
    }

    fputcsv($uchwyt, [...nagłówek...], ';', '"', '');

    foreach ($zadania as $zadanie) {
        fputcsv($uchwyt, [
            $zadanie['id'],
            // ... pozostałe klucze w kolejności kolumn ...
            $zadanie['wykonane'] ? 1 : 0,
            // ...
        ], ';', '"', '');
    }

    // zamknięcie i wynik
}
ROZWIĄZANIE
<?php

// Funkcje odczytu i zapisu plików z danymi TaskQuest (CSV, separator ;)

function zapiszZadania(string $sciezka, array $zadania): bool
{
    $uchwyt = fopen($sciezka, 'w');

    if ($uchwyt === false) {
        return false;
    }

    fputcsv($uchwyt, ['id', 'tytul', 'opis', 'termin', 'wykonane', 'xp'], ';', '"', '');

    foreach ($zadania as $zadanie) {
        fputcsv($uchwyt, [
            $zadanie['id'],
            $zadanie['tytul'],
            $zadanie['opis'],
            $zadanie['termin'],
            $zadanie['wykonane'] ? 1 : 0,
            $zadanie['xp']
        ], ';', '"', '');
    }

    fclose($uchwyt);

    return true;
}

Krok 4 — jednorazowa migracja

Stare zadania są jeszcze w data/zadania.php. Utwórz migracja.php (w głównym katalogu projektu), który:

  1. dołączy config/config.php, includes/funkcje_plikow.php i data/zadania.php (przez require_once i __DIR__),
  2. jeśli data/zadania.csv już istnieje — wypisze komunikat, że nie nadpisuje pliku, i nic nie zapisze,
  3. w przeciwnym razie zapisze $zadania do PLIK_ZADAN i wypisze, ile zadań zapisano (albo komunikat o błędzie, gdy zapiszZadania() zwróci false).

Punkt 2 jest ważny: to skrypt, który kasuje dane (tryb w). Gdyby ktoś uruchomił go drugi raz, po tym jak Kama zdążyła już poprawić zadania w CSV, jej zmiany zniknęłyby bez ostrzeżenia. Dobre narzędzie migracyjne samo chroni dane.

ROZWIĄZANIE
<?php

// Jednorazowe narzędzie programisty: przepisuje zadania z data/zadania.php do data/zadania.csv

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/data/zadania.php';   // tu jeszcze jest tablica $zadania

if (is_file(PLIK_ZADAN)) {
    echo "<p>Plik zadania.csv już istnieje — nie nadpisuję go.</p>";
} elseif (zapiszZadania(PLIK_ZADAN, $zadania)) {
    echo "<p>Zapisano " . count($zadania) . " zadań do pliku zadania.csv.</p>";
} else {
    echo "<p>Nie udało się zapisać pliku.</p>";
}

Uruchom migracja.php raz. Powinieneś/powinnaś zobaczyć „Zapisano 5 zadań do pliku zadania.csv.”. Uruchom drugi raz — teraz ma się pojawić komunikat o nieprzepisywaniu pliku.

Otwórz data/zadania.csv w edytorze kodu. Powinien wyglądać tak:

id;tytul;opis;termin;wykonane;xp
1;"   Nauczyć   się regexów   PHP   ";"Powtórzyć wyrażenia regularne przed następną lekcją.";2026-09-20;1;20
2;"Powtórzyć funkcje";"Przejrzeć wszystkie funkcje TaskQuest i sprawdzić ich parametry oraz typy zwracanych wartości.";2026-09-16;0;15
3;"Ćwiczenia z DateTime";"Napisać trzy własne przykłady z diff() i format().";2026-09-18;0;10
4;"Nauczyć się tablic PHP";"Zbudować listę zadań TaskQuest jako tablicę tablic asocjacyjnych.";2026-09-25;0;30
5;"Przygotować projekt do Lekcji 10";"Zastanowić się, jak podzielić TaskQuest na pliki i katalogi.";2026-09-22;1;25

Zwróć uwagę na tytuł pierwszego zadania — w pliku zostały spacje, które w data/zadania.php były celowo „brudne”. wyczyscTytul() nadal będzie miała co czyścić.

Krok 5 — SAMODZIELNIE: wczytajZadania()

Dopisz do includes/funkcje_plikow.php, pod zapiszZadania() (dwie puste linie odstępu, jak w pozostałych plikach), funkcję:

function wczytajZadania(string $sciezka): array

Funkcja ma:

  1. zwrócić pustą tablicę, jeśli plik nie jest plikiem albo nie da się go odczytać (is_file(), is_readable()),
  2. otworzyć plik w trybie r; jeśli fopen() zwróci false — zwrócić pustą tablicę,
  3. pominąć wiersz nagłówka,
  4. dla każdego kolejnego wiersza dodać do wyniku zadanie w takiej samej strukturze jak w data/zadania.php (z właściwymi typami — patrz tabela z Kroku 2),
  5. zamknąć plik i zwrócić tablicę zadań.
HINT

Nagłówek pomijasz jednym wywołaniem fgetcsv() przed pętlą (tak jak w zadaniu z odznakami). Nowe zadanie dopisujesz na koniec tablicy przez $zadania[] = [...].

POMOC
$zadania[] = [
    'id' => (int) $wiersz[0],
    'tytul' => $wiersz[1],
    // ... opis, termin ...
    'wykonane' => $wiersz[4] === '1',
    // ... xp ...
];
ROZWIĄZANIE
function wczytajZadania(string $sciezka): array
{
    $zadania = [];

    if (!is_file($sciezka) || !is_readable($sciezka)) {
        return $zadania;
    }

    $uchwyt = fopen($sciezka, 'r');

    if ($uchwyt === false) {
        return $zadania;
    }

    fgetcsv($uchwyt, 0, ';', '"', '');   // pierwszy wiersz to nagłówek — pomijamy go

    while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
        $zadania[] = [
            'id' => (int) $wiersz[0],
            'tytul' => $wiersz[1],
            'opis' => $wiersz[2],
            'termin' => $wiersz[3],
            'wykonane' => $wiersz[4] === '1',
            'xp' => (int) $wiersz[5]
        ];
    }

    fclose($uchwyt);

    return $zadania;
}

Krok 6 — index.php czyta zadania z pliku

W index.php zmieniamy dwa miejsca (reszta pliku zostaje bez zmian):

a) W bloku --- pliki aplikacji --- zamień linię

require_once __DIR__ . '/data/zadania.php';

na

require_once __DIR__ . '/includes/funkcje_plikow.php';

b) Pod blokiem --- dane użytkownika --- (a nad --- dane grywalizacji (symulowane) ---) dodaj nowy blok:

// --- dane zadań (z pliku CSV) ---
$zadania = wczytajZadania(PLIK_ZADAN);

Od tej chwili $zadania nie jest już zapisane w kodzie — jest wynikiem odczytu z pliku, a cała reszta index.php (pobierzAktywneZadania($zadania), sortujPoTerminie($zadania), pętla foreach…) działa tak jak wcześniej.

c) W includes/footer.php zmień TaskQuest 1.0 na TaskQuest 1.1.

Odśwież index.php. Strona ma wyglądać tak samo jak po Lekcji 10 (jedyna różnica: wersja w stopce). Jeśli widzisz „Brak zadań.” — sprawdź, czy data/zadania.csv istnieje i czy PLIK_ZADAN wskazuje właściwe miejsce.

7. Kolejne zadanie — odporność na „brudny” plik i diagnostyka

7.1 Pusta linia w środku pliku

Plik CSV edytuje człowiek — a ludzie zostawiają puste linie. Sprawdźmy, co się stanie.

Eksperyment przed poprawką: otwórz data/zadania.csv, wstaw pustą linię między wiersz z id 2 a wiersz z id 3 i zapisz plik. Odśwież index.php.

Zobaczysz kilka ostrzeżeń Undefined array key i błąd krytyczny w wyczyscTytul(), który dostał null zamiast tekstu. Dlaczego? fgetcsv() dla pustej linii zwraca tablicę z jednym polem (null), a my próbujemy odczytać z niej pola 1–5.

SAMODZIELNIE: popraw wczytajZadania() tak, żeby pomijała każdy wiersz, który nie ma dokładnie 6 pól (pusta linia albo uszkodzony wiersz). Użyj count() i continue.

HINT

Sprawdzenie robisz na samym początku pętli, zanim dotkniesz $wiersz[1].

ROZWIĄZANIE

W pętli while, jako pierwsza instrukcja:

if (count($wiersz) !== 6) {
    continue;   // pusta linia albo uszkodzony wiersz
}

Odśwież stronę — ostrzeżenia znikają, lista zadań jest kompletna. Usuń wstawioną pustą linię.

Świadome uproszczenie: uszkodzone wiersze są pomijane po cichu. Użytkownik nie dowie się, że coś zniknęło. Lepsze rozwiązanie (zgłoszenie problemu) da się zrobić dopiero z wyjątkami — Lekcja 12.

7.2 Diagnostyka zna nowe pliki

Strona diagnostyka.php sprawdza listę wymaganych plików — a ta lista jest już nieaktualna. Popraw ją:

  1. W $wymaganePliki zamień 'data/zadania.php' na dwa wpisy: 'includes/funkcje_plikow.php' i 'data/zadania.csv' (razem 11 plików).
  2. Z $katalogiDoPrzegladu usuń 'data' — w tym katalogu nie będzie już plików .php.
  3. Pod sekcją „Zawartość katalogów” dodaj nową sekcję „Pliki danych (.csv w data/)”, która wypisze liczbę i nazwy plików .csv z katalogu data/.

Efekt: 11 razy [OK] oraz sekcja pokazująca data/ — 1 plik(ów) i zadania.csv.

HINT

funkcja listaPlikowWKatalogu() ma drugi parametr — rozszerzenie. Pełny kod diagnostyka.php jest w rozwiązaniu referencyjnym (sekcja 10) — najpierw spróbuj samodzielnie.

8. Testy

Test 1 — zapis i odczyt dają te same dane

Do tej pory data/zadania.php jeszcze istnieje — wykorzystamy je jako wzorzec. W brudnopis.php:

<?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/data/zadania.php';

$plikTestowy = __DIR__ . '/data/test.csv';

zapiszZadania($plikTestowy, $zadania);
$odczytane = wczytajZadania($plikTestowy);

var_dump($odczytane === $zadania);

unlink($plikTestowy);

Oczekiwane: bool(true). Operator === na tablicach sprawdza klucze, wartości i typy — więc true oznacza, że int wrócił jako int, bool jako bool, a spacje w tytułach zostały zachowane. (unlink() usuwa plik testowy — pilnujemy, żeby zostawić po sobie porządek.)

Test 2 — trudne dane

Zastąp w powyższym kodzie $zadania własną tablicą i sprawdź, co się stanie z tekstem zawierającym średnik i cudzysłów:

$trudne = [[
    'id' => 7,
    'tytul' => 'Ćwiczenia; DateTime',
    'opis' => 'Powiedz "cześć"',
    'termin' => '2026-10-01',
    'wykonane' => false,
    'xp' => 5
]];

zapiszZadania($plikTestowy, $trudne);
echo file_get_contents($plikTestowy);
var_dump(wczytajZadania($plikTestowy) === $trudne);

Oczekiwane (usuń unlink(), jeśli chcesz obejrzeć plik):

id;tytul;opis;termin;wykonane;xp
7;"Ćwiczenia; DateTime";"Powiedz ""cześć""";2026-10-01;0;5
bool(true)

Cudzysłów wewnątrz pola jest zapisywany jako dwa cudzysłowy — i poprawnie odczytywany z powrotem.

Test 3 — różne zawartości pliku

Zanim ruszysz dalej, przewidź wynik każdego przypadku, a dopiero potem sprawdź. Pomocnik w brudnopis.php:

<?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';

$plikTestowy = __DIR__ . '/data/test.csv';
$naglowek = "id;tytul;opis;termin;wykonane;xp\n";

function ileZadan(string $plik, string $zawartosc): int
{
    file_put_contents($plik, $zawartosc);
    return count(wczytajZadania($plik));
}

echo ileZadan($plikTestowy, "") . "<br>";
echo ileZadan($plikTestowy, $naglowek) . "<br>";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n") . "<br>";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n\n2;c;d;2026-01-02;0;7\n") . "<br>";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1\n") . "<br>";
echo ileZadan($plikTestowy, "id,tytul\n1,a\n") . "<br>";

echo count(wczytajZadania(__DIR__ . '/data/nie_ma.csv'));

unlink($plikTestowy);
Zawartość plikuOczekiwany wynik
pusty plik0
tylko nagłówek0
nagłówek + 1 poprawny wiersz1
nagłówek + wiersz + pusta linia + wiersz2
wiersz z 5 polami zamiast 60
plik rozdzielony przecinkami0
plik nie istnieje0 (bez żadnego ostrzeżenia)

Test 4 — zmiana pliku zmienia aplikację

Bez dotykania kodu PHP. W data/zadania.csv zmień zadanie o id 2: wykonane z 0 na 1. Odśwież index.php.

Oczekiwane:

  • Wszystkie: 5 | Wykonane: 3 | Aktywne: 2
  • XP do zdobycia z aktywnych zadań: 40 (zostały zadania 3 i 4: 10 + 30)
  • komunikat o awansie na poziom 2
  • najpilniejsze zadanie: Ćwiczenia z DateTime
  • zadanie „Powtórzyć funkcje” jest oznaczone [X] i wyszarzone

Przywróć wartość 0.

Test 5 — przypadki brzegowe aplikacji

  1. Zostaw w data/zadania.csv tylko nagłówek → Wszystkie: 0, komunikat „Brak zadań.”, brak błędów.
  2. Tymczasowo zmień nazwę data/zadania.csv na data/zadania_x.csv → strona pokazuje „Brak zadań.” i nie wyświetla żadnych ostrzeżeń. Przywróć nazwę.
  3. Zastanów się: czy komunikat „Brak zadań.” w przypadku 2 jest prawdziwy? (Do tego wrócimy w pytaniach kontrolnych i w Lekcji 12.)

Test 6 — regresja: czy nic się nie zepsuło?

Uruchom obok siebie taskquest_lekcja10/index.php (kopia zapasowa) i nowy taskquest/index.php tego samego dnia. Poza wersją w stopce (1.0 → 1.1) treść ma być identyczna: te same liczniki, ten sam pasek, te same komunikaty terminów, ta sama kolejność zadań. Sprawdź też diagnostyka.php — 11 razy [OK].

Sprzątanie

Dopiero gdy testy 1–6 przeszły:

  1. Usuń migracja.php (drugie uruchomienie po usunięciu zadania.php skończyłoby się błędem, a jej zadanie jest wykonane).
  2. Usuń data/zadania.php — od teraz jedynym źródłem zadań jest plik CSV.
  3. Usuń pliki brudnopisu: brudnopis.php, dziennik.txt, gracze.csv, odznaki.csv.
  4. Odśwież index.php i diagnostyka.php — nadal ma działać, nadal 11 × [OK].

9. Obowiązkowe eksperymenty

Eksperymenty 1 i 3 wykonuj na plikach brudnopisu (dziennik.txt, data/test.csv), nie na zadania.csv.

Eksperyment 1: w kontra a

W rozwiązaniu z sekcji 4 zmień pierwsze otwarcie ('w') na 'a' i uruchom stronę trzy razy. Zajrzyj do dziennik.txt. Ile jest w nim linii Start TaskQuest i skąd się wzięły? Przywróć 'w', uruchom raz i porównaj. Który tryb wybraliśmy w zapiszZadania() i dlaczego dopisywanie nie miałoby tu sensu?

Eksperyment 2: zły separator

W wczytajZadania() zamień w obu wywołaniach fgetcsv() separator ';' na ',' i odśwież index.php. Co pokazuje strona i dlaczego (co zwraca fgetcsv(), gdy w linii nie ma przecinków, i który warunek zadziałał)? Przywróć ';'.

Wniosek: przy złym formacie pliku aplikacja nie zgłasza błędu — po cichu pokazuje „Brak zadań.”. To ta sama niepokojąca cisza, o której była mowa w Teście 5.

Eksperyment 3: bez (int)

W wczytajZadania() usuń rzutowanie (int) przy 'id' (zostaw samo $wiersz[0]) i uruchom Test 1. Jaki wynik dostajesz? Czy strona główna wygląda inaczej? Dopisz do brudnopisu:

var_dump($odczytane[0]['id']);

i porównaj typ z tym, co jest w $zadania[0]['id']. Co to mówi o różnicy między == i ===? Przywróć (int).

Eksperyment 4 (dodatkowy): plik tylko do odczytu

Zapisz data/test.csv przez zapiszZadania(), a następnie ustaw w systemie atrybut „tylko do odczytu” (Windows: Właściwości → Tylko do odczytu; Linux/macOS: chmod 444). Wywołaj zapiszZadania() jeszcze raz i wypisz wynik przez var_dump(). Co zwróciła funkcja i co zobaczyłaś/zobaczyłeś na ekranie oprócz tego? Zdejmij atrybut i usuń plik.

10. ROZWIĄZANIE — wersja referencyjna

Pełny kod TaskQuest po Lekcji 11. Nowe lub zmienione: config/config.php, includes/funkcje_plikow.php, data/zadania.csv, index.php, includes/footer.php, diagnostyka.php, migracja.php (jednorazowy). Bez zmian względem Lekcji 10: includes/header.php, includes/funkcje_grywalizacji.php, includes/funkcje_tekstowe.php, includes/funkcje_terminow.php, includes/funkcje_zadan.php, includes/funkcje_diagnostyczne.php.

ROZWIĄZANIE

config/config.php

<?php

// Ustawienia aplikacji TaskQuest
define('XP_NA_POZIOM', 100);
define('PLIK_ZADAN', dirname(__DIR__) . '/data/zadania.csv');

includes/funkcje_plikow.php

<?php

// Funkcje odczytu i zapisu plików z danymi TaskQuest (CSV, separator ;)

function wczytajZadania(string $sciezka): array
{
    $zadania = [];

    if (!is_file($sciezka) || !is_readable($sciezka)) {
        return $zadania;
    }

    $uchwyt = fopen($sciezka, 'r');

    if ($uchwyt === false) {
        return $zadania;
    }

    fgetcsv($uchwyt, 0, ';', '"', '');   // pierwszy wiersz to nagłówek — pomijamy go

    while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
        if (count($wiersz) !== 6) {
            continue;   // pusta linia albo uszkodzony wiersz
        }

        $zadania[] = [
            'id' => (int) $wiersz[0],
            'tytul' => $wiersz[1],
            'opis' => $wiersz[2],
            'termin' => $wiersz[3],
            'wykonane' => $wiersz[4] === '1',
            'xp' => (int) $wiersz[5]
        ];
    }

    fclose($uchwyt);

    return $zadania;
}


function zapiszZadania(string $sciezka, array $zadania): bool
{
    $uchwyt = fopen($sciezka, 'w');

    if ($uchwyt === false) {
        return false;
    }

    fputcsv($uchwyt, ['id', 'tytul', 'opis', 'termin', 'wykonane', 'xp'], ';', '"', '');

    foreach ($zadania as $zadanie) {
        fputcsv($uchwyt, [
            $zadanie['id'],
            $zadanie['tytul'],
            $zadanie['opis'],
            $zadanie['termin'],
            $zadanie['wykonane'] ? 1 : 0,
            $zadanie['xp']
        ], ';', '"', '');
    }

    fclose($uchwyt);

    return true;
}

data/zadania.csv

id;tytul;opis;termin;wykonane;xp
1;"   Nauczyć   się regexów   PHP   ";"Powtórzyć wyrażenia regularne przed następną lekcją.";2026-09-20;1;20
2;"Powtórzyć funkcje";"Przejrzeć wszystkie funkcje TaskQuest i sprawdzić ich parametry oraz typy zwracanych wartości.";2026-09-16;0;15
3;"Ćwiczenia z DateTime";"Napisać trzy własne przykłady z diff() i format().";2026-09-18;0;10
4;"Nauczyć się tablic PHP";"Zbudować listę zadań TaskQuest jako tablicę tablic asocjacyjnych.";2026-09-25;0;30
5;"Przygotować projekt do Lekcji 10";"Zastanowić się, jak podzielić TaskQuest na pliki i katalogi.";2026-09-22;1;25

index.php

<?php

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';


// --- dane użytkownika ---
$imieUzytkownika = "Kama";
$login = "Kama_2026";


// --- dane zadań (z pliku CSV) ---
$zadania = wczytajZadania(PLIK_ZADAN);


// --- dane grywalizacji (symulowane) ---
$xp = 99;
$streak = 5;
$dataOstatniejAktywnosci = "2026-09-14";


// --- wywołania funkcji: grywalizacja ---
$poziom = obliczPoziom($xp);
$tytul = pobierzTytul($poziom);
$xpWAktualnymPoziomie = obliczXpWAktualnymPoziomie($xp, $poziom);
$procentPostepu = obliczProcentPostepu($xpWAktualnymPoziomie);
$brakujeXp = ileBrakujeXp($xp, $poziom);

//pasek postępu
$liczbaAktywnychSegmentow = (int) ($procentPostepu / 10);
$progNastepnegoPoziomu = $poziom * XP_NA_POZIOM;


// --- wywołania funkcji: lista zadań ---
$wykonaneZadania = pobierzWykonaneZadania($zadania);
$aktywneZadania = pobierzAktywneZadania($zadania);

$liczbaWszystkich = count($zadania);
$liczbaWykonanych = count($wykonaneZadania);
$liczbaAktywnych = count($aktywneZadania);

$xpDoZdobycia = sumujXp($aktywneZadania);

$zadaniaWgTerminu = sortujPoTerminie($zadania);
$aktywneWgTerminu = sortujPoTerminie($aktywneZadania);

// uproszczenie (symulacja): aktywność = wykonane jakiekolwiek zadanie z listy
$zadanieWykonane = $liczbaWykonanych > 0;

$streak = aktualizujStreak($streak, $dataOstatniejAktywnosci, $zadanieWykonane);


// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <p>Zdobywaj punkty za wykonane zadania!</p>

        <?php
        if (walidujLogin($login)) {
            echo "<p>Login poprawny: $login</p>";
        } else {
            echo "<p>Login jest niepoprawny.</p>";
        }
        ?>

        <h2>Witaj, <?= $imieUzytkownika; ?>!</h2>

        <p>Poziom: <strong><?= $poziom; ?></strong></p>
        <p>Tytuł: <strong><?= $tytul; ?></strong></p>
        <p>Łączne XP: <strong><?= $xp; ?></strong></p>

        <h3>
            <?= "Masz $xpWAktualnymPoziomie XP z " . XP_NA_POZIOM . " XP potrzebnych do poziomu " . ($poziom + 1) . " (" . round($procentPostepu, 2) . "%)"; ?>
        </h3>

        <div class="pasek-xp">
            <?php
            for ($segment = 1; $segment <= 10; $segment++) {
                if ($segment <= $liczbaAktywnychSegmentow) {
                    echo "█";
                } else {
                    echo "░";
                }
            }
            ?>
        </div>

        <p>Kolejny próg: <?= $progNastepnegoPoziomu; ?> XP</p>
        <p>Brakuje XP: <strong><?= $brakujeXp; ?></strong></p>
        <p>Streak: <strong><?= $streak ?></strong></p>

        <h2>Twoje zadania</h2>

        <p>
            Wszystkie: <strong><?= $liczbaWszystkich; ?></strong> |
            Wykonane: <strong><?= $liczbaWykonanych; ?></strong> |
            Aktywne: <strong><?= $liczbaAktywnych; ?></strong>
        </p>
        <p>XP do zdobycia z aktywnych zadań: <strong><?= $xpDoZdobycia; ?></strong></p>

        <?php
        if ($liczbaWszystkich === 0) {
            echo "<p>Brak zadań.</p>";
        } elseif ($liczbaAktywnych === 0) {
            echo "<p>Wszystkie zadania wykonane — świetna robota!</p>";
        } else {
            if ($xpDoZdobycia >= $brakujeXp) {
                echo "<p class=\"awans\">Wykonując aktywne zadania, awansujesz na poziom " . ($poziom + 1) . "!</p>";
            } else {
                echo "<p>Aktywne zadania nie wystarczą jeszcze na awans — brakuje " . ($brakujeXp - $xpDoZdobycia) . " XP.</p>";
            }

            $najpilniejsze = $aktywneWgTerminu[0];
            echo "<p>Najpilniejsze zadanie: <strong>" . wyczyscTytul($najpilniejsze['tytul']) . "</strong> — "
                . komunikatTerminu($najpilniejsze['termin']) . "</p>";
        }

        echo "<h3>Lista zadań (wg terminu)</h3>";

        foreach ($zadaniaWgTerminu as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie['tytul']);
            $znacznik = $zadanie['wykonane'] ? "[X]" : "[ ]";
            $klasa = $zadanie['wykonane'] ? "zadanie wykonane" : "zadanie";

            echo "<div class=\"$klasa\">";
            echo "<p><strong>$znacznik $tytulCzysty</strong> — {$zadanie['xp']} XP</p>";
            echo "<p>" . skrocOpis($zadanie['opis'], 50) . "</p>";
            echo "<p>Termin: {$zadanie['termin']}";

            if (!$zadanie['wykonane']) {
                echo " — " . komunikatTerminu($zadanie['termin']);
            }

            echo "</p>";

            if (stripos($tytulCzysty, 'PHP') !== false) {
                echo "<p>To zadanie dotyczy PHP.</p>";
            }

            echo "</div>";
        }
        ?>
<?php
require_once __DIR__ . '/includes/footer.php';

includes/footer.php

        <p>TaskQuest 1.1</p>
    </div>
</body>
</html>

diagnostyka.php

<?php

require_once __DIR__ . '/includes/funkcje_diagnostyczne.php';

$wymaganePliki = [
    'index.php',
    'config/config.php',
    'includes/header.php',
    'includes/footer.php',
    'includes/funkcje_grywalizacji.php',
    'includes/funkcje_tekstowe.php',
    'includes/funkcje_terminow.php',
    'includes/funkcje_zadan.php',
    'includes/funkcje_diagnostyczne.php',
    'includes/funkcje_plikow.php',
    'data/zadania.csv'
];

$katalogiDoPrzegladu = ['config', 'includes'];

$nazwaStrony = 'TaskQuest — diagnostyka';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Diagnostyka projektu</h2>

        <h3>Wymagane pliki</h3>
        <?php
        foreach ($wymaganePliki as $plik) {
            $znacznik = czyPlikIstnieje($plik) ? "[OK]" : "[BRAK]";
            echo "<p>$znacznik $plik</p>";
        }
        ?>

        <h3>Zawartość katalogów (pliki .php)</h3>
        <?php
        foreach ($katalogiDoPrzegladu as $katalog) {
            $pliki = listaPlikowWKatalogu($katalog);

            echo "<p><strong>$katalog/</strong> — " . count($pliki) . " plik(ów)</p>";

            foreach ($pliki as $plik) {
                echo "<p>&nbsp;&nbsp;$plik</p>";
            }
        }
        ?>

        <h3>Pliki danych (.csv w data/)</h3>
        <?php
        $plikiCsv = listaPlikowWKatalogu('data', 'csv');

        echo "<p><strong>data/</strong> — " . count($plikiCsv) . " plik(ów)</p>";

        foreach ($plikiCsv as $plik) {
            echo "<p>&nbsp;&nbsp;$plik</p>";
        }
        ?>
<?php
require_once __DIR__ . '/includes/footer.php';

migracja.php (tylko do jednorazowego użycia, potem usuń)

<?php

// Jednorazowe narzędzie programisty: przepisuje zadania z data/zadania.php do data/zadania.csv

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/data/zadania.php';   // tu jeszcze jest tablica $zadania

if (is_file(PLIK_ZADAN)) {
    echo "<p>Plik zadania.csv już istnieje — nie nadpisuję go.</p>";
} elseif (zapiszZadania(PLIK_ZADAN, $zadania)) {
    echo "<p>Zapisano " . count($zadania) . " zadań do pliku zadania.csv.</p>";
} else {
    echo "<p>Nie udało się zapisać pliku.</p>";
}

11. Co zmieniliśmy w TaskQuest?

Zostaje

$imieUzytkownika, $login
$xp, $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp,
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu
$streak, $dataOstatniejAktywnosci, $zadanieWykonane
$zadania, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $xpDoZdobycia
$nazwaStrony
define('XP_NA_POZIOM', 100)

wszystkie 16 funkcji z poprzednich lekcji (kod bez zmian)
struktura tablicy $zadania (klucze: id, tytul, opis, termin, wykonane, xp)

Dochodzi

define('PLIK_ZADAN', ...)                 // config/config.php

wczytajZadania(), zapiszZadania()          // includes/funkcje_plikow.php

nowe pliki: includes/funkcje_plikow.php, data/zadania.csv
            (migracja.php — użyty raz i usunięty)

Nowe narzędzia PHP: fopen(), fclose(), fgets(), fwrite(), fgetcsv(), fputcsv(), is_readable(), unlink(), tryby r / w / a, file_put_contents(), file_get_contents(), file(), rzutowanie (int) przy odczycie.

Zmieniamy

  • źródło zadań: $zadania nie pochodzi już z data/zadania.php, tylko z wczytajZadania(PLIK_ZADAN) w index.php,
  • index.php: jedna linia require_once zamieniona na inną, plus nowy blok „dane zadań (z pliku CSV)”,
  • diagnostyka.php: lista wymaganych plików (11), sekcja plików .csv w data/, 'data' zniknęło z $katalogiDoPrzegladu,
  • includes/footer.php: TaskQuest 1.1.

Usuwamy

  • data/zadania.php — dane przeniesione do CSV; plik był od początku oznaczony jako tymczasowy,
  • migracja.php — jednorazowe narzędzie, którego zadanie jest wykonane (i które przy ponownym użyciu mogłoby nadpisać dane).

Żadna zmienna ani funkcja z Lekcji 10 nie została usunięta.

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 11 zadania TaskQuest nie są już częścią kodu: leżą w pliku data/zadania.csv, który można edytować w edytorze tekstu bez dotykania PHP, a aplikacja odczytuje go przy każdym uruchomieniu i zamienia z powrotem na tablicę $zadania o właściwych typach. TaskQuest potrafi też taką listę zapisać do pliku i nie wywraca się na pustej linii w pliku. Na razie nie ma jeszcze formularza — zapis wywołujemy z kodu.

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Wiem, co robią tryby r, w i a — i który z nich kasuje zawartość pliku.
[ ] Każde otwarcie pliku kończę fclose() i sprawdzam wynik fopen() (=== false).
[ ] Wiem, dlaczego czytam linie przez while (($linia = fgets(...)) !== false).
[ ] Potrafię zapisać i odczytać wiersz CSV (fputcsv, fgetcsv) z separatorem ;.
[ ] Wiem, że z pliku wraca tekst, i potrafię odtworzyć int oraz bool.
[ ] Mam includes/funkcje_plikow.php z wczytajZadania() i zapiszZadania().
[ ] PLIK_ZADAN jest zdefiniowany w config/config.php.
[ ] data/zadania.csv ma nagłówek i 5 zadań; index.php odczytuje je z pliku.
[ ] wczytajZadania() pomija puste linie i wiersze o złej liczbie pól.
[ ] Test 1 (=== po zapisie i odczycie) daje bool(true).
[ ] Strona główna wygląda jak po Lekcji 10 (jedyna różnica: TaskQuest 1.1).
[ ] diagnostyka.php pokazuje 11 razy [OK].
[ ] Usunęłam/usunąłem migracja.php, data/zadania.php i pliki brudnopisu.
[ ] Przeprowadziłam/przeprowadziłem eksperymenty i przywróciłam/przywróciłem kod.

Pytania kontrolne

  1. Wymień trzy kroki pracy z plikiem. Co może pójść nie tak przy pierwszym z nich i jak sprawdzasz, czy poszło dobrze?
  2. Plik dziennik.txt ma trzy linie. Co w nim zostanie, jeśli otworzysz go w trybie w i od razu zamkniesz (bez żadnego zapisu), a co, jeśli otworzysz go w trybie a i od razu zamkniesz?
  3. Dlaczego pętla odczytu ma postać while (($linia = fgets($uchwyt)) !== false), a nie while ($linia = fgets($uchwyt))? Podaj przykład zawartości pliku, przy której druga wersja zawiedzie.
  4. Linia pliku: 3;"Ćwiczenia; DateTime";"Opis";2026-09-18;0;10. Ile pól zwróci fgetcsv()? Ile zwróciłaby, gdyby w pliku nie było cudzysłowów?
  5. Po odczycie $wiersz[4] ma wartość "0". Jakiego jest typu? Co da (bool) $wiersz[4], a co $wiersz[4] === '1'? Co dałoby (bool) dla wpisu nie?
  6. Po co przed pętlą w wczytajZadania() jest samotne wywołanie fgetcsv()? Co stałoby się z aplikacją bez niego? (Wskazówka: co trafiłoby do 'termin' pierwszego „zadania” i która funkcja by na tym poległa?)
  7. zapiszZadania() otwiera plik w trybie w. Co się stanie z danymi, jeśli skrypt przerwie działanie w połowie zapisu? Jakiego problemu nie mielibyśmy przy dopisywaniu (a), a jakiego byśmy się dorobili?
  8. wczytajZadania() zwraca [] zarówno wtedy, gdy plik jest pusty, jak i wtedy, gdy plik nie istnieje. Dlaczego to niedobre dla użytkownika? Jak — bez wyjątków, których jeszcze nie znamy — mogłaby wyglądać funkcja, która odróżnia te dwa przypadki?
  9. Dlaczego migracja.php sprawdza is_file(PLIK_ZADAN) przed zapisem? Opisz sytuację, w której brak tego sprawdzenia kosztowałby Kamę dane.
  10. Dlaczego funkcje z Lekcji 9 (sortujPoTerminie(), pobierzAktywneZadania()…) nie wymagały żadnej zmiany, mimo że dane pochodzą teraz z pliku? Co to mówi o dobrym podziale kodu na funkcje?

Zadanie końcowe — rozbuduj TaskQuest samodzielnie

Wybierz jedno (albo oba):

A) Kopia zapasowa zadań. Napisz funkcję zrobKopieZadan(string $sciezka): bool (w includes/funkcje_plikow.php), która skopiuje plik zadań do data/zadania_RRRR-MM-DD.csv z dzisiejszą datą w nazwie (new DateTime('today') i format('Y-m-d') znasz z lekcji o datach). Do kopiowania użyj funkcji copy($zrodlo, $cel) — zwraca true lub false. Wywołaj ją z osobnego skryptu kopia.php i wypisz komunikat o wyniku. Dopisz plik kopia.php do listy w diagnostyce tylko wtedy, gdy uznasz go za stałą część projektu.

B) Dziennik uruchomień. Przy każdym wejściu na index.php dopisuj linię do pliku logs/taskquest.log w formacie 2026-09-21 10:15:03 — wczytano 5 zadań. Katalog logs/ utwórz przez mkdir() (najpierw is_dir(), tak jak w zadaniu B z Lekcji 10), a dopisywanie zrób przez file_put_contents() z flagą FILE_APPEND. Przemyśl, po której linii w index.php dopisanie ma sens (co ma już wtedy być policzone?).

Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.

Most do Lekcji 12

TaskQuest czyta zadania z pliku — ale co, jeśli coś pójdzie nie tak?

data/zadania.csv nie istnieje       →  "Brak zadań."   (prawda?)
plik ma zły format                  →  "Brak zadań."   (prawda?)
plik jest uszkodzony w środku       →  uszkodzone wiersze znikają po cichu
zapiszZadania() nie może otworzyć   →  zwraca false, ale nikt tego nie sprawdza

Nasze funkcje potrafią tylko „zwrócić pustą tablicę i mieć nadzieję”. Użytkownik nie dowiaduje się, że to awaria, a nie brak zadań.

Następny problem:

Jak sprawić, żeby TaskQuest zauważył problem, przerwał to, co robi, i zareagował — zamiast udawać, że wszystko jest w porządku?

To temat Lekcji 12: wyjątki (try, catch, throw).


PODSUMOWANIE LEKCJI

W skrócie

Praca z plikiem to zawsze trzy kroki: fopen() → odczyt/zapis → fclose(). fopen() zwraca uchwyt (albo false) i wszystkie pozostałe funkcje dostają ten uchwyt, a nie nazwę pliku.

Tryb otwarcia decyduje o wszystkim. r – tylko odczyt, plik musi istnieć. w – kasuje zawartość w momencie otwarcia, nawet jeśli nic nie zapiszesz. a – dopisuje na końcu, zawartość zostaje. Oba (w, a) tworzą plik, jeśli go nie ma. r na nieistniejącym pliku daje false.

Każda operacja na pliku może się nie udać. fopen() zwraca false (brak pliku, brak uprawnień, zła ścieżka). Dlatego: przed otwarciem is_file() i is_readable(), po otwarciu if ($uchwyt === false). Wywołanie fgets() na false to już błąd krytyczny.

Pętla odczytu ma postać while (($linia = fgets($uchwyt)) !== false). Samo while ($linia = fgets(...)) zatrzyma się na linii o treści 0 lub pustej — "0" jest w PHP traktowane jak false. fgets() zostawia na końcu \n, więc zwykle potrzebny jest trim().

Z pliku wraca wyłącznie tekst. "250" to nie 250, a "1" to nie true. Typy trzeba odtworzyć ręcznie: (int) $wiersz[0] dla liczb, $wiersz[4] === '1' dla wartości logicznych. (bool) się nie nadaje — (bool) "nie" daje true, bo każdy niepusty tekst poza "0" jest prawdą.

CSV to zwykły tekst z separatorem. fputcsv() sam otacza cudzysłowami pola zawierające spacje, separator lub cudzysłów, a fgetcsv() je z powrotem rozbiera — dlatego tytuł Ćwiczenia; DateTime nie rozpadnie się na dwa pola. W TaskQuest separatorem jest średnik, więc komplet parametrów ';', '"', '' podajemy przy każdym wywołaniu (bez trzech ostatnich argumentów PHP 8.4 zgłasza Deprecated, a domyślnym separatorem jest przecinek, nie średnik).

Skróty są dla prostych przypadków. file_put_contents(), file_get_contents(), file() robią całą robotę jedną linią (otwarcie, operacja, zamknięcie). Bez flagi FILE_APPEND file_put_contents() nadpisuje plik. Gdy dane trzeba czytać linia po linii i rozbierać na pola — wygodniejszy jest fopen().

Źródła błędów, które kosztują dane: tryb w na jedynej kopii ważnego pliku, copy() na istniejący plik docelowy (nadpisuje), unlink() (usuwa trwale, bez kosza), edycja pliku .csv w Excelu (zmienia format dat, separator i kodowanie), ścieżka względna zamiast __DIR__, zapomniane fclose().


Co trzeba umieć na INF.03?

  • napisać kompletny zapis do pliku i odczyt z pliku w schemacie fopen() → fwrite() / fgets() → fclose(),
  • znać skutki trybów r, w, a i umieć wskazać, który z nich kasuje dane,
  • rozpoznać w cudzym kodzie brak sprawdzenia wyniku fopen() oraz pętlę odczytu, która zgubi ostatnią linię,
  • użyć skrótów file_put_contents() (z FILE_APPEND), file_get_contents(), file() do prostych zadań, np. dopisania wpisu do logu,
  • odczytać dane tabelaryczne z pliku tekstowego do tablicy (fgetcsv() lub samodzielny podział linii) i odtworzyć typy ((int), porównanie z '1'),
  • znać funkcje pomocnicze: file_exists(), is_file(), is_readable(), is_writable(), filesize(), filemtime(), copy(), rename(), unlink().

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. fopen() → praca → fclose(). Uchwyt, nie nazwa pliku, trafia do kolejnych funkcji.
  2. w kasuje plik przy otwarciu. a dopisuje. r nie tworzy pliku i zwraca false, gdy pliku nie ma.
  3. Po fopen() zawsze if ($uchwyt === false), przed fopen() — is_file() i is_readable().
  4. Pętla odczytu: while (($linia = fgets($uchwyt)) !== false). Nigdy while ($linia = fgets(...)).
  5. Z pliku wraca string. (int) $pole dla liczb, $pole === '1' dla bool. Nie (bool).
  6. fputcsv() / fgetcsv() z kompletem ';', '"', ''; w fgetcsv() drugi argument to 0 (bez limitu).
  7. Pierwszy fgetcsv() przed pętlą pomija wiersz nagłówka.
  8. file_put_contents($plik, $tekst, FILE_APPEND) = dopisanie jedną linią. Bez FILE_APPEND — nadpisanie.
  9. unlink() usuwa nieodwracalnie, copy() nadpisuje cel, rename() przenosi i zmienia nazwę.
  10. Ścieżki budujemy z __DIR__ (albo ze stałej z config.php, jak PLIK_ZADAN).

Ściągawka – najważniejsze polecenia i składnia

Schemat zapisu

$uchwyt = fopen(__DIR__ . '/dziennik.txt', 'a');   // 'w' = od zera, 'a' = dopisz

if ($uchwyt === false) {
    return false;
}

fwrite($uchwyt, "Nowa linia\n");
fclose($uchwyt);

Schemat odczytu

if (!is_file($sciezka) || !is_readable($sciezka)) {
    return [];
}

$uchwyt = fopen($sciezka, 'r');

while (($linia = fgets($uchwyt)) !== false) {
    echo trim($linia) . '<br>';
}

fclose($uchwyt);

CSV

fputcsv($uchwyt, ['id', 'tytul', 'xp'], ';', '"', '');   // tablica → linia
$wiersz = fgetcsv($uchwyt, 0, ';', '"', '');             // linia → tablica lub false

Odtwarzanie typów po odczycie

'id'       => (int) $wiersz[0],
'wykonane' => $wiersz[4] === '1',

Tryby otwarcia

TrybOdczytZapisGdy pliku nie maIstniejąca zawartość
rtakniefalsenietknięta
r+taktakfalsenietknięta
wnietaktworzykasowana
w+taktaktworzykasowana
anietak (na końcu)tworzyzostaje
a+taktak (na końcu)tworzyzostaje
xnietaktworzyfalse, jeśli plik istnieje

Skróty jednoliniowe

file_put_contents($plik, $tekst);                 // zapis (nadpisuje)
file_put_contents($plik, $tekst, FILE_APPEND);    // dopisanie
$tekst  = file_get_contents($plik);               // cały plik jako tekst
$linie  = file($plik, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

Sprawdzanie i operacje na plikach

file_exists($p)  is_file($p)  is_readable($p)  is_writable($p)  is_dir($p)
filesize($p)     filemtime($p)                 // rozmiar, czas modyfikacji (timestamp)
copy($z, $c)     rename($s, $n)     unlink($p)  // kopiuj, przenieś/zmień nazwę, usuń

Pozostałe funkcje odczytu i wskaźnik

fread($uchwyt, filesize($plik));   // N bajtów (cały plik)
fgetc($uchwyt);                    // jeden znak
feof($uchwyt);                     // czy koniec pliku
rewind($uchwyt);                   // wskaźnik na początek

Blokada przy współbieżnym zapisie (np. licznik odwiedzin)

flock($uchwyt, LOCK_EX);   // wyłączna (zapis); LOCK_SH = współdzielona (odczyt)
// ... odczyt, rewind(), fwrite() ...
fflush($uchwyt);
flock($uchwyt, LOCK_UN);

Quiz – sprawdź się

Odpowiedzi na końcu dodatku.

1. Który tryb otwarcia sprawia, że dotychczasowa zawartość pliku zostaje nietknięta, a nowe dane trafiają na jego koniec?
a) r+ b) w c) a d) x

2. Co znajdzie się w pliku a.txt po wykonaniu tego kodu i dlaczego?

$u = fopen(__DIR__ . '/a.txt', 'w');
fwrite($u, "raz\n");
fclose($u);

$u = fopen(__DIR__ . '/a.txt', 'w');
fwrite($u, "dwa\n");
fclose($u);

3. Plik liczby.txt zawiera trzy linie: 5, 0, 7. Co wypisze poniższy kod, a co powinien wypisywać? Popraw jeden fragment.

$u = fopen(__DIR__ . '/liczby.txt', 'r');

while ($linia = fgets($u)) {
    echo trim($linia) . '|';
}

fclose($u);

4. Linia pliku ma postać 3;Ćwiczenia;2026-09-18. Ile elementów będzie miała tablica zwrócona przez fgetcsv($u) wywołane bez dodatkowych argumentów? Co zrobić, żeby otrzymać trzy pola?

5. Po odczycie $wiersz[4] ma wartość "0". Które wyrażenie poprawnie odtworzy pole wykonane i dlaczego drugie jest złym pomysłem?
a) (bool) $wiersz[4] b) $wiersz[4] === '1'
Co zwróci każde z nich, jeśli w pliku zamiast 0 znajdzie się tekst nie?

6. W funkcji wczytajZadania() ktoś usunął jedną linię — samotne wywołanie fgetcsv() przed pętlą. Co dokładnie znajdzie się w tablicy $zadania jako pierwszy element po tej zmianie i która funkcja z wcześniejszych lekcji zachowa się przez to dziwnie?

7. Dlaczego ten kod przerywa działanie skryptu błędem krytycznym, jeśli plik brak.txt nie istnieje? Dopisz dwie linie, które to naprawią.

$u = fopen(__DIR__ . '/brak.txt', 'r');

while (($l = fgets($u)) !== false) {
    echo $l;
}

fclose($u);

8. Ile linii będzie miał plik log.txt po wykonaniu tego kodu? Popraw go tak, aby powstały trzy wpisy.

foreach (['start', 'wczytano', 'koniec'] as $wpis) {
    file_put_contents(__DIR__ . '/log.txt', $wpis . "\n");
}

9. Napisz jedną linię, która dopisze do pliku logs/taskquest.log wpis w formacie 2026-09-21 10:15:03 — wczytano 5 zadan (liczba zadań jest w $liczbaWszystkich, datę weź z date('Y-m-d H:i:s')).

10. Dobierz funkcję do zadania: (a) zarchiwizuj zadania.csv jako zadania_2026-09-28.csv, zostawiając oryginał; (b) przenieś stare.csv do katalogu archiwum/; (c) skasuj plik tymczasowy. Przy której z nich grozi cicha utrata danych i dlaczego?

11. Funkcja zapiszZadania() otwiera plik w trybie w. Co konkretnie zepsułoby się w pliku zadania.csv, gdyby zmienić ten tryb na a i wywołać funkcję dwa razy?


Odpowiedzi
  1. c) a. w kasuje zawartość już w momencie otwarcia, r+ ustawia wskaźnik na początku i nadpisuje od pierwszego znaku, x zwróci false, bo plik istnieje.
  2. Tylko dwa. Drugie fopen(..., 'w') skasowało plik przy otwarciu — linia raz przepadła, zanim cokolwiek zapisano.
  3. Wypisze 5| i zatrzyma się. Druga linia ma treść "0", która w warunku jest traktowana jak false, więc pętla kończy się za wcześnie — powinno być 5|0|7|. Poprawka: while (($linia = fgets($u)) !== false).
  4. Jeden element — całą linię jako jeden napis, bo domyślnym separatorem fgetcsv() jest przecinek, a w pliku jest średnik. Trzeba podać fgetcsv($u, 0, ';', '"', '').
  5. b). (bool) "0" faktycznie daje false, ale to przypadek: (bool) "nie" daje true, bo każdy niepusty tekst poza "0" jest prawdą. $wiersz[4] === 'nie' porównane z '1' daje false — jednoznacznie i zgodnie z zamiarem. Odpowiedź dla nie: a) true, b) false.
  6. Pierwszym elementem $zadania stanie się wiersz nagłówka: 'id' => (int) 'id', czyli 0, 'tytul' => 'tytul', 'termin' => 'termin', 'wykonane' => false (bo 'wykonane' !== '1'), 'xp' => 0. Dziwnie zachowają się funkcje terminów z Lekcji 7/9 — 'termin' nie jest datą, więc próba utworzenia z niego DateTime / obliczenia różnicy się nie powiedzie. Zepsuje się też sortowanie po terminie i licznik XP.
  7. fopen() zwraca false (plik nie istnieje) i wypisuje ostrzeżenie. Następnie fgets(false) dostaje wartość logiczną zamiast uchwytu — to już błąd krytyczny (TypeError). Naprawa:
if (!is_file($sciezka) || !is_readable($sciezka)) {
    return;                      // albo: return [];
}

$u = fopen($sciezka, 'r');

if ($u === false) {
    return;
}
  1. Jedną — koniec. Bez flagi każde wywołanie nadpisuje plik. Poprawka: file_put_contents(__DIR__ . '/log.txt', $wpis . "\n", FILE_APPEND);
  2. file_put_contents(__DIR__ . '/logs/taskquest.log', date('Y-m-d H:i:s') . " — wczytano $liczbaWszystkich zadan\n", FILE_APPEND); (katalog logs/ musi istnieć — if (!is_dir(...)) { mkdir(...); }).
  3. (a) copy(), (b) rename(), (c) unlink(). Cicha utrata danych grozi przy copy() i rename(): jeśli plik docelowy istnieje, zostaje nadpisany bez pytania. unlink() też jest nieodwracalny (plik nie trafia do kosza), ale tu akurat usunięcie jest zamierzone. Bezpieczny nawyk: przed zapisem sprawdzić is_file($cel).
  4. Przy drugim wywołaniu plik miałby dwa wiersze nagłówka (drugi w środku pliku) i zdublowane wszystkie zadania. Przy odczycie pomijany jest tylko pierwszy wiersz, więc nagłówek ze środka trafiłby do $zadania jako pseudo-zadanie (patrz pytanie 6), a każde zadanie pojawiłoby się na liście dwa razy — z podwojonym XP do zdobycia.