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
require_once. Plik z funkcjami zawsze z_once:requiredołączyłby go dwa razy, a druga deklaracja tej samej funkcji to błąd krytyczny („Cannot redeclare…”).requirezamiastinclude, bo bez tych funkcji aplikacja nie ma sensu./var/www/taskquest.__DIR__to/var/www/taskquest/includes, adirname()cofa o jeden poziom — stąd ten zapis w funkcjach diagnostycznych.- Pojawią się dwa dodatkowe wpisy:
.(ten katalog) i..(katalog nadrzędny).scandir()zawsze je zwraca; pomijamy je warunkiemif ($element === '.' || $element === '..') { continue; }. bool(false)—datajest katalogiem, ais_file()sprawdza, czy ścieżka wskazuje plik. Dla katalogu prawdę zwróciis_dir().- Katalog już istnieje, więc
mkdir()zgłasza ostrzeżenie i zwracafalse. Wzorzec: najpierw sprawdź, potem działaj —if (!is_dir($katalog)) { mkdir($katalog); }. - 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”.
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:
- Otwieramy plik lub go tworzymy
- Zapisujemy dane w pliku
- Zamykamy plik
Odczytywanie danych z pliku również przebiega 3-etapowo:
- Otwieramy plik, a jeżeli takowy nie może zostać otwarty musi zostać oprogramowane zgłoszenie błędu
- Odczytujemy dane
- 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\nz końca liniiFILE_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
| Kategoria | Funkcja | Opis |
|---|---|---|
| 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_APPENDdo 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:
- Wygeneruj 20 losowych liczb (1-100)
- Zapisz do
liczby.txtz blokowaniem - Odczytaj i oblicz:
- sumę
- średnią
- min i max
- liczby parzyste/nieparzyste
- Zapisz statystyki do
statystyki.txt
Zadanie 2
Losowanie liczb w stylu Lotto z zapisem.
Wymagania:
- Wygeneruj 6 unikalnych liczb (1-49)
- Pobierz aktualną datę
- Zapisz do
liczby.txtw formacie:2025-10-13 15:20:00 wylosowano liczby: 3, 15, 22, 31, 42, 45 - 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ć trybyr,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 windex.php— to nadal symulacja.
Rozgrzewka — 4 pytania z Lekcji 10
Odpowiedz z pamięci, zanim zaczniesz:
- Czym różni się
requireodrequire_oncei którego używamy dla plików z funkcjami? - Co przechowuje
__DIR__w plikuincludes/header.php? - Co zwraca
scandir()oprócz nazw plików i jak radzi sobie z tymlistaPlikowWKatalogu()? - 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
| Plik | Zawartość |
|---|---|
config/config.php | stała PLIK_ZADAN — ścieżka do pliku z zadaniami |
includes/funkcje_plikow.php | wczytajZadania(), zapiszZadania() |
data/zadania.csv | zadania jako zwykły tekst: jedno zadanie = jedna linia |
migracja.php | jednorazowe przepisanie starej tablicy do CSV |
Przygotowanie (zrób to teraz)
- Zrób kopię zapasową całego katalogu
taskquest/z Lekcji 10 — np. jakotaskquest_lekcja10/(obok, nie w środku). Przyda się do testu regresji. - Uruchom
index.phpi 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:
- miejsca na dane, które nie jest kodem — pliku, który można otworzyć w edytorze i poprawić,
- sposobu, żeby PHP odczytało ten plik i zamieniło go z powrotem na tablicę
$zadania, - 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ć:
| Tryb | Co robi | Gdy pliku nie ma | Uwaga |
|---|---|---|---|
r | tylko odczyt | fopen() zwraca false | plik nietknięty |
w | zapis od zera | plik jest tworzony | od razu kasuje całą dotychczasową zawartość |
a | dopisywanie na końcu | plik jest tworzony | dotychczasowa 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, zwracafalse.- W nawiasie jest przypisanie i porównanie naraz: najpierw
$linia = fgets(...), potem wynik jest porównywany zfalse. - Dlaczego
!== false, a nie samowhile ($linia = fgets(...))? Bo ostatnia linia pliku może mieć treść"0", a"0"w warunku jest traktowane jakfalse— pętla zatrzymałaby się o linię za wcześnie.!== falsejest odporne na taki przypadek. fgets()zostawia na końcu linii znak\n, dlatego czyścimy go przeztrim().
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:
- przed otwarciem sprawdzamy:
is_file()(z Lekcji 10 — czy to plik) orazis_readable()(czy PHP ma prawo go czytać), - 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:
| Funkcja | Co 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 (albofalsena 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 pliku | Jak 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
wdo jedynej kopii ważnego pliku — dane znikają w momencie otwarcia, nawet jeśli potem nic nie zapiszesz. - Brak sprawdzenia
=== falsepofopen(). - Edycja CSV w Excelu: program potrafi po zapisie zmienić format dat (
2026-09-20na20.09.2026), separator i kodowanie. Pliki.csvw 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:
- otworzy
dziennik.txtw trybiewi zapisze linięStart TaskQuest, - otworzy ten sam plik w trybie
ai dopisze dwie linie:Wczytano zadaniaorazKoniec, - otworzy plik w trybie
ri 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:
- zapisze do
odznaki.csvnagłówekid;nazwa;poziomi trzy odznaki:1 / Pierwszy krok / 1,2 / Mistrz; regexów / 2,3 / Legenda TaskQuest / 5, - odczyta plik (pomijając nagłówek) i wypisze każdą odznakę w formie
Nazwa — wymagany poziom N, - 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:
| Kolumna | Klucz w $zadania | Typ | Jak odtwarzamy |
|---|---|---|---|
id | 'id' | int | (int) |
tytul | 'tytul' | string | bez zmian (surowy — czyści go wyczyscTytul() przy wyświetlaniu) |
opis | 'opis' | string | bez 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:
- otworzyć plik w trybie
w; jeśli się nie da — zwrócićfalse, - zapisać wiersz nagłówka
id;tytul;opis;termin;wykonane;xp, - zapisać każde zadanie z
$zadaniajako jeden wiersz (w kolejności kolumn z Kroku 2;wykonanejako1lub0), - 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:
- dołączy
config/config.php,includes/funkcje_plikow.phpidata/zadania.php(przezrequire_oncei__DIR__), - jeśli
data/zadania.csvjuż istnieje — wypisze komunikat, że nie nadpisuje pliku, i nic nie zapisze, - w przeciwnym razie zapisze
$zadaniadoPLIK_ZADANi wypisze, ile zadań zapisano (albo komunikat o błędzie, gdyzapiszZadania()zwrócifalse).
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:
- zwrócić pustą tablicę, jeśli plik nie jest plikiem albo nie da się go odczytać (
is_file(),is_readable()), - otworzyć plik w trybie
r; jeślifopen()zwrócifalse— zwrócić pustą tablicę, - pominąć wiersz nagłówka,
- 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), - 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ą:
- W
$wymaganePlikizamień'data/zadania.php'na dwa wpisy:'includes/funkcje_plikow.php'i'data/zadania.csv'(razem 11 plików). - Z
$katalogiDoPrzegladuusuń'data'— w tym katalogu nie będzie już plików.php. - Pod sekcją „Zawartość katalogów” dodaj nową sekcję „Pliki danych (.csv w data/)”, która wypisze liczbę i nazwy plików
.csvz katalogudata/.
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ść pliku | Oczekiwany wynik |
|---|---|
| pusty plik | 0 |
| tylko nagłówek | 0 |
| nagłówek + 1 poprawny wiersz | 1 |
| nagłówek + wiersz + pusta linia + wiersz | 2 |
| wiersz z 5 polami zamiast 6 | 0 |
| plik rozdzielony przecinkami | 0 |
| plik nie istnieje | 0 (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: 2XP 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
- Zostaw w
data/zadania.csvtylko nagłówek →Wszystkie: 0, komunikat „Brak zadań.”, brak błędów. - Tymczasowo zmień nazwę
data/zadania.csvnadata/zadania_x.csv→ strona pokazuje „Brak zadań.” i nie wyświetla żadnych ostrzeżeń. Przywróć nazwę. - 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:
- Usuń
migracja.php(drugie uruchomienie po usunięciuzadania.phpskończyłoby się błędem, a jej zadanie jest wykonane). - Usuń
data/zadania.php— od teraz jedynym źródłem zadań jest plik CSV. - Usuń pliki brudnopisu:
brudnopis.php,dziennik.txt,gracze.csv,odznaki.csv. - Odśwież
index.phpidiagnostyka.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> $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> $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ń:
$zadanianie pochodzi już zdata/zadania.php, tylko zwczytajZadania(PLIK_ZADAN)windex.php, index.php: jedna liniarequire_oncezamieniona na inną, plus nowy blok „dane zadań (z pliku CSV)”,diagnostyka.php: lista wymaganych plików (11), sekcja plików.csvwdata/,'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ę$zadaniao 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
- Wymień trzy kroki pracy z plikiem. Co może pójść nie tak przy pierwszym z nich i jak sprawdzasz, czy poszło dobrze?
- Plik
dziennik.txtma trzy linie. Co w nim zostanie, jeśli otworzysz go w trybiewi od razu zamkniesz (bez żadnego zapisu), a co, jeśli otworzysz go w trybieai od razu zamkniesz? - Dlaczego pętla odczytu ma postać
while (($linia = fgets($uchwyt)) !== false), a niewhile ($linia = fgets($uchwyt))? Podaj przykład zawartości pliku, przy której druga wersja zawiedzie. - Linia pliku:
3;"Ćwiczenia; DateTime";"Opis";2026-09-18;0;10. Ile pól zwrócifgetcsv()? Ile zwróciłaby, gdyby w pliku nie było cudzysłowów? - 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 wpisunie? - Po co przed pętlą w
wczytajZadania()jest samotne wywołaniefgetcsv()? 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?) zapiszZadania()otwiera plik w trybiew. 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?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?- Dlaczego
migracja.phpsprawdzais_file(PLIK_ZADAN)przed zapisem? Opisz sytuację, w której brak tego sprawdzenia kosztowałby Kamę dane. - 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,ai 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()(zFILE_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:
fopen()→ praca →fclose(). Uchwyt, nie nazwa pliku, trafia do kolejnych funkcji.wkasuje plik przy otwarciu.adopisuje.rnie tworzy pliku i zwracafalse, gdy pliku nie ma.- Po
fopen()zawszeif ($uchwyt === false), przedfopen()—is_file()iis_readable(). - Pętla odczytu:
while (($linia = fgets($uchwyt)) !== false). Nigdywhile ($linia = fgets(...)). - Z pliku wraca string.
(int) $poledla liczb,$pole === '1'dlabool. Nie(bool). fputcsv()/fgetcsv()z kompletem';', '"', ''; wfgetcsv()drugi argument to0(bez limitu).- Pierwszy
fgetcsv()przed pętlą pomija wiersz nagłówka. file_put_contents($plik, $tekst, FILE_APPEND)= dopisanie jedną linią. BezFILE_APPEND— nadpisanie.unlink()usuwa nieodwracalnie,copy()nadpisuje cel,rename()przenosi i zmienia nazwę.- Ścieżki budujemy z
__DIR__(albo ze stałej zconfig.php, jakPLIK_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
| Tryb | Odczyt | Zapis | Gdy pliku nie ma | Istniejąca zawartość |
|---|---|---|---|---|
r | tak | nie | false | nietknięta |
r+ | tak | tak | false | nietknięta |
w | nie | tak | tworzy | kasowana |
w+ | tak | tak | tworzy | kasowana |
a | nie | tak (na końcu) | tworzy | zostaje |
a+ | tak | tak (na końcu) | tworzy | zostaje |
x | nie | tak | tworzy | false, 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
- c)
a.wkasuje zawartość już w momencie otwarcia,r+ustawia wskaźnik na początku i nadpisuje od pierwszego znaku,xzwrócifalse, bo plik istnieje. - Tylko
dwa. Drugiefopen(..., 'w')skasowało plik przy otwarciu — liniarazprzepadła, zanim cokolwiek zapisano. - Wypisze
5|i zatrzyma się. Druga linia ma treść"0", która w warunku jest traktowana jakfalse, więc pętla kończy się za wcześnie — powinno być5|0|7|. Poprawka:while (($linia = fgets($u)) !== false). - Jeden element — całą linię jako jeden napis, bo domyślnym separatorem
fgetcsv()jest przecinek, a w pliku jest średnik. Trzeba podaćfgetcsv($u, 0, ';', '"', ''). - b).
(bool) "0"faktycznie dajefalse, ale to przypadek:(bool) "nie"dajetrue, bo każdy niepusty tekst poza"0"jest prawdą.$wiersz[4] === 'nie'porównane z'1'dajefalse— jednoznacznie i zgodnie z zamiarem. Odpowiedź dlanie: a)true, b)false. - Pierwszym elementem
$zadaniastanie się wiersz nagłówka:'id' => (int) 'id', czyli0,'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 niegoDateTime/ obliczenia różnicy się nie powiedzie. Zepsuje się też sortowanie po terminie i licznik XP. fopen()zwracafalse(plik nie istnieje) i wypisuje ostrzeżenie. Następniefgets(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;
}
- Jedną —
koniec. Bez flagi każde wywołanie nadpisuje plik. Poprawka:file_put_contents(__DIR__ . '/log.txt', $wpis . "\n", FILE_APPEND); file_put_contents(__DIR__ . '/logs/taskquest.log', date('Y-m-d H:i:s') . " — wczytano $liczbaWszystkich zadan\n", FILE_APPEND);(kataloglogs/musi istnieć —if (!is_dir(...)) { mkdir(...); }).- (a)
copy(), (b)rename(), (c)unlink(). Cicha utrata danych grozi przycopy()irename(): 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). - 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
$zadaniajako pseudo-zadanie (patrz pytanie 6), a każde zadanie pojawiłoby się na liście dwa razy — z podwojonym XP do zdobycia.