10. PHP – operacje na strukturze systemu plików


Szybki quiz – przypomnienie poprzedniej lekcji

1. Co zostanie wyświetlone?

$zadanie = ['tytul' => 'PHP', 'xp' => 20];
echo count($zadanie) . " " . $zadanie['xp'];

2. Znajdź błąd. Co zobaczysz po uruchomieniu?

$zadania = [
    ['tytul' => 'A', 'xp' => 10],
    ['tytul' => 'B', 'xp' => 5]
];

foreach ($zadania as $zadanie) {
    echo $zadania['tytul'];
}

3. Jaki będzie wynik?

$terminy = ['2026-10-02', '2026-09-20'];

usort($terminy, function ($a, $b) {
    return $a <=> $b;
});

echo $terminy[0];

A co wyświetli ten sam kod, jeśli daty zapiszemy jako '02.10.2026' i '20.09.2026'? Czy wynik będzie poprawny?

4. Dlaczego ten kod nie wypisze posortowanej listy?

$posortowane = usort($zadania, function ($zadanieA, $zadanieB) {
    return $zadanieA['termin'] <=> $zadanieB['termin'];
});

print_r($posortowane);

5. Uzupełnij, aby funkcja zwracała sumę XP wszystkich zadań:

return array_sum(array_column($zadania, ______));

6. Co zwróci sumujXp([]) w wersji z pętlą foreach z Lekcji 9? Dlaczego nie ma błędu?

Odpowiedzi
  1. 2 20 — tablica ma dwa elementy (klucze tytul i xp).
  2. W pętli użyto $zadania (cała lista) zamiast $zadanie (jedno zadanie). Lista nie ma klucza 'tytul', więc dla każdego obiegu pojawi się ostrzeżenie o niezdefiniowanym kluczu i nic nie zostanie wypisane. Poprawnie: echo $zadanie['tytul'];.
  3. 2026-09-20. Przy zapisie '02.10.2026' i '20.09.2026' na początku będzie '02.10.2026' — błędnie, bo teksty są porównywane znak po znaku, a '0' jest „mniejsze” niż '2'. Format RRRR-MM-DD tego problemu nie ma.
  4. usort() zmienia tablicę, na której pracuje, i nie zwraca nowej tablicy (zwraca true). print_r($posortowane) wypisze 1. Poprawnie: wywołać usort($zadania, ...), a potem print_r($zadania) — albo użyć funkcji sortujPoTerminie(), która zwraca posortowaną kopię.
  5. 'xp'
  6. 0 — pętla nie wykona się ani razu, więc funkcja zwraca początkową wartość $suma = 0.

TaskQuest

Dodatek

Dołączanie plików

Jeżeli nasz skrypt zawiera dużo kodu możemy go podzielić na mniejsze partie, a później „sklejać” w całość. Po co? Zobaczmy.

  • Lepsze zarządzanie projektem
  • Wielokrotne użycie kodu (DRY – Don’t Repeat Yourself)
  • Łatwiejsze utrzymanie i aktualizacja
  • Separacja odpowiedzialności
  • Współpraca w zespole
dołączanie pliku

Wiemy już jaki jest sens dzielenia pliku, ale jak to później połączyć? ano tak:

include()

dołącza plik do kodu, w razie braku pliku generuje ostrzeżenie kontynuując pracę skryptu

include('header.php');
include('functions.php');

// Skrypt działa dalej, nawet jeśli pliku nie ma
echo "Strona działa";

Kiedy używać:

  • Pliki opcjonalne (np. moduły, które mogą nie istnieć)
  • Elementy strony, których brak nie blokuje całości

include_once()

dołącza plik do kodu jednokrotnie, w razie braku pliku generuje ostrzeżenie kontynuując pracę skryptu

include_once('config.php');
include_once('config.php');  // Zostanie zignorowane

Po co:

  • Zapobiega wielokrotnemu dołączeniu tego samego pliku
  • Unika redefinicji funkcji i klas

require()

dołącza plik do kodu, w razie braku pliku generuje błąd i przerywa działanie skryptu

require('config.php');
require('database.php');

// Jeśli któryś plik nie istnieje, skrypt się zatrzyma
echo "Ta linia może się nie wykonać";

Kiedy używać:

  • Pliki krytyczne (konfiguracja, baza danych)
  • Elementy bez których aplikacja nie może działać

require_once()

dołącza plik do kodu jednokrotnie, w razie braku pliku generuje błąd i przerywa działanie skryptu

require_once('config.php');
require_once('config.php');  // Zostanie zignorowane

Najczęściej używane – łączy bezpieczeństwo require z ochroną przed wielokrotnym dołączeniem.

Porównanie funkcji

Funkcja Przy braku pliku Wielokrotne dołączenie
include Warning, kontynuuje ✓ Dozwolone
include_once Warning, kontynuuje ✗ Tylko raz
require Fatal Error, przerywa ✓ Dozwolone
require_once Fatal Error, przerywa ✗ Tylko raz

Składnia ścieżek

// Ścieżka względna - od bieżącego pliku
require_once('header.php');
require_once('./includes/config.php');
require_once('../parent-dir/file.php');

// Ścieżka bezwzględna - od roota serwera
require_once('/var/www/html/config.php');

// Ze zmienną __DIR__ (zalecane)
require_once(__DIR__ . '/includes/config.php');

// Przykład
$base = __DIR__;
require_once($base . '/config/database.php');
require_once($base . '/includes/functions.php');

Używaj __DIR__ dla pewności, że ścieżka jest poprawna niezależnie od miejsca wywołania.

Modularyzacja kodu

Przykład struktury projektu

projekt/
├── index.php
├── config/
│   ├── config.php
│   └── database.php
├── includes/
│   ├── header.php
│   ├── footer.php
│   └── menu.php
├── functions/
│   ├── auth.php
│   └── helpers.php
└── assets/
    ├── css/
    ├── js/
    └── images/

 

Nasze pliki wyglądają teraz tak:

header.php

<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?? 'Moja Strona' ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header>
<h1>Moja Strona WWW</h1>
</header>

menu.php

<nav id="menu">
<ul>
<li><a href="index.php">Start</a></li>
<li><a href="about.php">O nas</a></li>
<li><a href="contact.php">Kontakt</a></li>
<li><a href="blog.php">Blog</a></li>
</ul>
</nav>

footer.php

    <footer>
<p>&copy; <?= date('Y') ?> Moja Strona. Wszelkie prawa zastrzeżone.</p>
</footer>

<script src="assets/js/main.js"></script>
</body>
</html>

index.php

<?php
// Konfiguracja
require_once(__DIR__ . '/config/config.php');

// Ustawienia strony
$pageTitle = 'Strona główna';

// Dołącz nagłówek i menu
require_once(__DIR__ . '/includes/header.php');
require_once(__DIR__ . '/includes/menu.php');
?>

<main>
<section>
<h2>Witamy na stronie głównej</h2>
<p>To jest treść strony głównej.</p>
</section>
</main>

<?php
// Dołącz stopkę
require_once(__DIR__ . '/includes/footer.php');
?>

config.php

<?php
// Konfiguracja aplikacji
define('SITE_NAME', 'Moja Strona');
define('SITE_URL', 'https://example.com');
define('SITE_EMAIL', 'kontakt@example.com');

// Ustawienia bazy danych
define('DB_HOST', 'localhost');
define('DB_NAME', 'mojabaza');
define('DB_USER', 'root');
define('DB_PASS', '');

// Strefa czasowa
date_default_timezone_set('Europe/Warsaw');

// Raportowanie błędów (development)
error_reporting(E_ALL);
ini_set('display_errors', 1);

Użycie:

<?php
require_once(__DIR__ . '/includes/Template.php');

$template = new Template();
$template->setTitle('Strona główna');
$template->addStyle('assets/css/style.css');
$template->addScript('assets/js/main.js');

$template->header();
?>

<h1>Treść strony</h1>

<?php
$template->footer();
?>

Operacje na katalogach

is_dir() / is_file()

Sprawdzają, czy ścieżka wskazuje na katalog lub plik.

$path = 'uploads';

if (is_dir($path)) {
    echo "$path jest katalogiem";
}

if (is_file('config.php')) {
    echo "config.php jest plikiem";
}

// Sprawdź przed operacją
if (is_dir('logs')) {
    echo "Katalog logs istnieje";
} else {
    mkdir('logs');
    echo "Katalog logs został utworzony";
}

getcwd() / chdir()

Pobieranie i zmiana bieżącego katalogu roboczego.

// Bieżący katalog
echo getcwd(); // np. /var/www/html

// Zmień katalog
chdir('public_html');
echo getcwd(); // /var/www/html/public_html

// Powrót do poprzedniego
chdir('..');
echo getcwd(); // /var/www/html

scandir()

Wypisze wszystkie pliki i katalogi z określonej lokalizacji. Pierwszy parametr to lokalizacja katalogu, drugi to sortowanie: 0 (rosnąco – domyślny) lub 1(malejąco)

$dir = 'uploads';
$files = scandir($dir);

print_r($files);
/*
Array
(
[0] => .
[1] => ..
[2] => image1.jpg
[3] => document.pdf
[4] => subfolder
)
*/

// Sortowanie malejąco
$files = scandir($dir, SCANDIR_SORT_DESCENDING);

// Pomiń . i ..
$files = array_diff(scandir($dir), ['.', '..']);

// Wyświetl tylko pliki
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_file($path)) {
echo "Plik: $file\n";
}
}

mkdir() i rmdir()

Tworzenie katalogu:

Przykład:

// Prosty katalog
if (mkdir('uploads')) {
echo "Katalog utworzony";
}

// Z prawami dostępu (Unix/Linux)
mkdir('cache', 0755);

// Rekurencyjne tworzenie (zagnieżdżone)
mkdir('a/b/c/d', 0755, true);

// Sprawdź czy istnieje przed utworzeniem
if (!is_dir('logs')) {
mkdir('logs', 0755, true);
}

Z parametrami (np. prawa dostępu i tworzenie zagnieżdżonych katalogów):

mkdir('a/b/c', 0777, true);  // true → rekurencyjnie tworzy strukturę

Prawa dostępu (Unix/Linux):

  • 0755 – właściciel: rwx, grupa: r-x, inni: r-x
  • 0777 – wszyscy: rwx (rzadko zalecane ze względów bezpieczeństwa)
  • 0700 – tylko właściciel: rwx

Usuwanie katalogu:

Przykład:

// Usuń pusty katalog
if (rmdir('temp')) {
echo "Katalog usunięty";
}

// rmdir() działa TYLKO dla pustych katalogów!

// Usuń katalog z zawartością (rekurencyjnie)
function rmdirRecursive($dir) {
if (!is_dir($dir)) {
return false;
}

$files = array_diff(scandir($dir), ['.', '..']);

foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? rmdirRecursive($path) : unlink($path);
}

return rmdir($dir);
}

// Użycie
rmdirRecursive('old_folder');

rmdir() działa tylko, jeśli katalog jest pusty.

Funkcje ścieżek — basename(), dirname(), pathinfo(), realpath()

Służą do analizowania ścieżek bez potrzeby operowania na stringach ręcznie.

basename() — zwraca nazwę pliku

$path = '/var/www/html/index.php';

echo basename($path);  // index.php

// Bez rozszerzenia
echo basename($path, '.php');  // index

dirname() — zwraca katalog nadrzędny

$path = '/var/www/html/index.php';

echo dirname($path);  // /var/www/html

// Poziom wyżej
echo dirname($path, 2);  // /var/www (PHP 7.0+)

pathinfo() — zwraca tablicę z częściami ścieżki

$path = '/var/www/html/images/photo.jpg';

$info = pathinfo($path);
print_r($info);
/*
Array
(
    [dirname] => /var/www/html/images
    [basename] => photo.jpg
    [extension] => jpg
    [filename] => photo
)
*/

// Konkretna część
echo pathinfo($path, PATHINFO_EXTENSION);  // jpg
echo pathinfo($path, PATHINFO_FILENAME);   // photo
echo pathinfo($path, PATHINFO_DIRNAME);    // /var/www/html/images
echo pathinfo($path, PATHINFO_BASENAME);   // photo.jpg

realpath() – bezwzględna ścieżka

// Bieżący katalog: /var/www/html

echo realpath('.');           // /var/www/html
echo realpath('..');          // /var/www
echo realpath('./config.php'); // /var/www/html/config.php

// Sprawdź czy plik istnieje
if (realpath('config.php') !== false) {
    echo "Plik istnieje";
}

Nowoczesne stałe ścieżek (PHP 5.3+)

// __DIR__ - katalog bieżącego pliku
echo __DIR__;  // /var/www/html

// __FILE__ - pełna ścieżka bieżącego pliku
echo __FILE__;  // /var/www/html/index.php

// Zastosowanie
require_once(__DIR__ . '/config/database.php');
require_once(__DIR__ . '/../includes/functions.php');

// Bezpieczne budowanie ścieżek
$configPath = __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.php';

Otwieranie katalogów — opendir(), readdir(), closedir()

Służy do przeglądania zawartości katalogów, np. listowania plików.

$dir = 'uploads';

if ($handle = opendir($dir)) {
    echo "Zawartość katalogu $dir:<br>";
    
    while (($file = readdir($handle)) !== false) {
        // Pomiń . i ..
        if ($file != '.' && $file != '..') {
            $type = is_dir("$dir/$file") ? 'katalog' : 'plik';
            echo "• $file ($type)<br>";
        }
    }
    
    closedir($handle);
}

Nowoczesna alternatywa – scandir():

$dir = 'uploads';
$files = array_diff(scandir($dir), ['.', '..']);

foreach ($files as $file) {
    $path = "$dir/$file";
    $type = is_dir($path) ? 'katalog' : 'plik';
    echo "• $file ($type)<br>";
}

Podsumowanie

OperacjaFunkcjaUwagi
Blokowanie plikuflock()Zapobiega konfliktom przy zapisie
Kopiowaniecopy()Nadpisuje, jeśli plik istnieje
Przenoszenie / zmiana nazwyrename()Działa dla plików i katalogów
Usuwanie plikuunlink()Bez kosza
Tworzenie katalogumkdir()Obsługuje tryby i zagnieżdżenie
Usuwanie katalogurmdir()Katalog musi być pusty
Analiza ścieżekbasename(), dirname(), pathinfo()Bardzo pomocne przy pracy z uploadem
Przeglądanie katalogówopendir(), readdir(), closedir()Iteracja po plikach

Nowoczesne podejście

SPL DirectoryIterator (PHP 5.0+)

$dir = 'uploads';

foreach (new DirectoryIterator($dir) as $file) {
    if ($file->isDot()) continue;  // pomiń . i ..
    
    echo $file->getFilename();
    
    if ($file->isDir()) {
        echo " [katalog]";
    } elseif ($file->isFile()) {
        echo " [plik, " . $file->getSize() . " bajtów]";
    }
    
    echo "\n";
}

RecursiveDirectoryIterator – przeglądanie rekurencyjne

$dir = 'project';

$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($dir),
    RecursiveIteratorIterator::SELF_FIRST
);

foreach ($iterator as $file) {
    if ($file->isFile()) {
        echo $file->getPathname() . "\n";
    }
}

FilesystemIterator – więcej opcji

$dir = 'uploads';

$iterator = new FilesystemIterator($dir, 
    FilesystemIterator::SKIP_DOTS  // pomiń . i ..
);

foreach ($iterator as $file) {
    echo $file->getFilename() . "\n";
}

Glob – wyszukiwanie wzorców

// Wszystkie pliki PHP
$phpFiles = glob('*.php');
print_r($phpFiles);

// Wszystkie obrazy
$images = glob('images/*.{jpg,jpeg,png,gif}', GLOB_BRACE);

// Rekurencyjne wyszukiwanie
$allPhp = glob('**/*.php', GLOB_BRACE);

// Przykład: usuń wszystkie pliki tymczasowe
foreach (glob('temp/*.tmp') as $file) {
    unlink($file);
}

Tabela funkcji – szybka ściąga

 

OperacjaFunkcjaOpis
Dołączanie  
 include()Dołącz, warning przy błędzie
 include_once()Dołącz raz, warning
 require()Dołącz, fatal error przy błędzie
 require_once()Dołącz raz, fatal error
Katalogi  
 is_dir()Czy to katalog?
 mkdir()Utwórz katalog
 rmdir()Usuń pusty katalog
 scandir()Lista plików/katalogów
 getcwd()Bieżący katalog
 chdir()Zmień katalog
Ścieżki  
 basename()Nazwa pliku
 dirname()Katalog nadrzędny
 pathinfo()Szczegóły ścieżki
 realpath()Bezwzględna ścieżka

Dobre praktyki

1. Zawsze używaj require_once dla plików krytycznych

// ŹLE
include('config.php');

// DOBRZE
require_once(__DIR__ . '/config/config.php');

2. Używaj DIR zamiast względnych ścieżek

// ŹLE - może nie działać
require_once('../includes/header.php');

// DOBRZE - zawsze działa
require_once(__DIR__ . '/../includes/header.php');

3. Sprawdzaj istnienie katalogów przed operacjami

// ŹLE
mkdir('cache');

// DOBRZE
if (!is_dir('cache')) {
    mkdir('cache', 0755, true);
}

4. Używaj separatora katalogów

// ŹLE - nie działa na Windows
$path = '/var/www/html/config.php';

// DOBRZE - uniwersalne
$path = __DIR__ . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';

// Lub po prostu /
$path = __DIR__ . '/config/config.php';  // działa na wszystkich systemach

5. Nigdy nie ufaj danym użytkownika w ścieżkach

// ŹLE - podatne na Path Traversal
$file = $_GET['file'];
include("pages/$file.php");  // Użytkownik może wpisać: ../../config/database

// DOBRZE - walidacja
$file = basename($_GET['file']);  // usuwa ścieżki
$allowed = ['home', 'about', 'contact'];

if (in_array($file, $allowed)) {
    include("pages/$file.php");
}

Przykłady praktyczne

1. Prosty system szablonów

functions/template.php:

<?php
function renderTemplate(string $templateName, array $data = []): void {
    $templatePath = __DIR__ . '/../templates/' . $templateName . '.php';
    
    if (!file_exists($templatePath)) {
        throw new Exception("Template not found: $templateName");
    }
    
    // Wyciągnij zmienne z tablicy
    extract($data);
    
    // Dołącz szablon
    require($templatePath);
}

templates/page.php:

<!DOCTYPE html>
<html lang="pl">
<head>
    <title><?= htmlspecialchars($title ?? 'Strona') ?></title>
</head>
<body>
    <h1><?= htmlspecialchars($heading) ?></h1>
    <p><?= htmlspecialchars($content) ?></p>
</body>
</html>

index.php:

<?php
require_once(__DIR__ . '/functions/template.php');

renderTemplate('page', [
    'title' => 'Moja Strona',
    'heading' => 'Witamy',
    'content' => 'To jest treść strony.'
]);

2. Automatyczne ładowanie plików (Autoloader)

<?php
// autoload.php

spl_autoload_register(function($className) {
    $baseDir = __DIR__ . '/classes/';
    $file = $baseDir . str_replace('\\', '/', $className) . '.php';
    
    if (file_exists($file)) {
        require_once($file);
    }
});

// Użycie
require_once('autoload.php');

$user = new User();  // automatycznie załaduje classes/User.php
$db = new Database(); // automatycznie załaduje classes/Database.php

3. Galeria zdjęć z katalogu

<?php
function displayGallery(string $dir): void {
    $extensions = ['jpg', 'jpeg', 'png', 'gif'];
    $images = [];
    
    foreach ($extensions as $ext) {
        $images = array_merge($images, glob("$dir/*.$ext"));
    }
    
    if (empty($images)) {
        echo "<p>Brak zdjęć w galerii.</p>";
        return;
    }
    
    echo '<div class="gallery">';
    foreach ($images as $image) {
        $filename = basename($image);
        echo '<div class="gallery-item">';
        echo "<img src='$image' alt='$filename' loading='lazy'>";
        echo "<p>$filename</p>";
        echo '</div>';
    }
    echo '</div>';
}

// Użycie
displayGallery('images/gallery');
?>

<style>
.gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
}

.gallery-item img {
    width: 100%;
    height: 200px;
    object-fit: cover;
}
</style>

4. Lista plików do pobrania

<?php
function listDownloads(string $dir): void {
    if (!is_dir($dir)) {
        echo "Katalog nie istnieje.";
        return;
    }
    
    $files = array_diff(scandir($dir), ['.', '..']);
    
    if (empty($files)) {
        echo "<p>Brak plików do pobrania.</p>";
        return;
    }
    
    echo '<ul class="downloads">';
    foreach ($files as $file) {
        $path = "$dir/$file";
        
        if (is_file($path)) {
            $size = filesize($path);
            $sizeFormatted = formatBytes($size);
            $extension = pathinfo($path, PATHINFO_EXTENSION);
            
            echo '<li>';
            echo "<a href='$path' download>";
            echo "<strong>$file</strong> ($sizeFormatted, .$extension)";
            echo '</a>';
            echo '</li>';
        }
    }
    echo '</ul>';
}

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

// Użycie
listDownloads('downloads');

5. Cache systemu

<?php
class FileCache {
    private string $cacheDir;
    private int $ttl;  // Time To Live w sekundach
    
    public function __construct(string $cacheDir = 'cache', int $ttl = 3600) {
        $this->cacheDir = $cacheDir;
        $this->ttl = $ttl;
        
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
    }
    
    public function get(string $key): mixed {
        $file = $this->getCacheFile($key);
        
        if (!file_exists($file)) {
            return null;
        }
        
        // Sprawdź czy nie wygasł
        if (time() - filemtime($file) > $this->ttl) {
            unlink($file);
            return null;
        }
        
        $content = file_get_contents($file);
        return unserialize($content);
    }
    
    public function set(string $key, mixed $value): bool {
        $file = $this->getCacheFile($key);
        $content = serialize($value);
        return file_put_contents($file, $content) !== false;
    }
    
    public function delete(string $key): bool {
        $file = $this->getCacheFile($key);
        
        if (file_exists($file)) {
            return unlink($file);
        }
        
        return false;
    }
    
    public function clear(): void {
        $files = glob($this->cacheDir . '/*');
        
        foreach ($files as $file) {
            if (is_file($file)) {
                unlink($file);
            }
        }
    }
    
    private function getCacheFile(string $key): string {
        return $this->cacheDir . '/' . md5($key) . '.cache';
    }
}

// Użycie
$cache = new FileCache('cache', 3600);  // 1 godzina TTL

// Zapisz
$cache->set('users', ['Jan', 'Anna', 'Piotr']);

// Odczytaj
$users = $cache->get('users');

// Usuń
$cache->delete('users');

// Wyczyść wszystko
$cache->clear();

Zadanie dla klas, które miały HTMLa:

Rozpocznij tworzenie autorskiego CMS – możesz wykorzystać do tego stronę www, którą posiadasz (nie WordPress, Joomla itp.).

Wydziel z niej bloki odpowiedzialne za formatowanie tych części strony, które się nie zmieniają (jak w przykładzie wyżej) tzn.:

  • nagłówek
  • menu
  • stopka
  • część główna strony
  • panele boczne
  • itp.

TaskQuest 10 — aplikacja przestaje być jednym plikiem

KARTA PRACY

Plik TaskQuest z Lekcji 9 ma już kilkaset linii: stałą, kilkanaście funkcji, dane, wywołania, style i HTML w jednym miejscu. Żeby zmienić próg XP, musisz przewinąć pół pliku. A to dopiero początek — w kolejnych lekcjach dojdą formularze, baza danych i logowanie.

Cel lekcji

Nauczyć TaskQuest:

  • dzielić kod na osobne pliki i „sklejać” go z powrotem (include, include_once, require, require_once),
  • budować niezawodne ścieżki do plików (__DIR__, dirname()),
  • sprawdzać i przeglądać strukturę własnego projektu (is_file(), is_dir(), scandir(), pathinfo(), mkdir()).

Lekcja ma dwie części:

  1. Porządki — dzielimy TaskQuest na pliki. Strona główna ma wyglądać i działać dokładnie tak samo jak po Lekcji 9. To świadoma refaktoryzacja: użytkownik nie widzi różnicy, ale programista widzi ogromną.
  2. Diagnostyka — dokładamy stronę diagnostyka.php, która sprawdza, czy pliki projektu leżą tam, gdzie powinny. To pierwsza nowa funkcja TaskQuest 1.0.

Uwaga: dane nadal są zapisane „na sztywno” (symulacja). Plik data/zadania.php jest na tym etapie tymczasowy — w Lekcji 11 zastąpimy go prawdziwym plikiem z danymi.

Rozgrzewka — 4 pytania z Lekcji 9

Odpowiedz z pamięci, zanim zaczniesz:

  1. Dlaczego $aktywneZadania[0] może nie istnieć, a $aktywneWgTerminu[0] istnieje?
  2. Co musi zwrócić funkcja podana do array_filter(), żeby element został w wyniku?
  3. Jak zamienić sortowanie w sortujPoTerminie() z rosnącego na malejące?
  4. Czym różni się $tytul od $zadanie['tytul']?

1. Wracamy do naszego TaskQuest

Mamy aplikację, która liczy poziom i XP, waliduje login, czyści tytuły, pokazuje terminy, liczy streak i wyświetla posortowaną listę zadań. Wszystkie zmienne i funkcje zostają. Zmienia się tylko to, w którym pliku leżą.

Docelowa struktura projektu

taskquest/
├── index.php
├── diagnostyka.php                  (nowe)
├── config/
│   └── config.php
├── includes/
│   ├── header.php
│   ├── footer.php
│   ├── funkcje_grywalizacji.php
│   ├── funkcje_tekstowe.php
│   ├── funkcje_terminow.php
│   ├── funkcje_zadan.php
│   └── funkcje_diagnostyczne.php    (nowe)
└── data/
    └── zadania.php

Co gdzie trafi

PlikZawartość
config/config.phpdefine('XP_NA_POZIOM', 100);
includes/funkcje_grywalizacji.phpobliczPoziom(), pobierzTytul(), obliczXpWAktualnymPoziomie(), obliczProcentPostepu(), ileBrakujeXp()
includes/funkcje_tekstowe.phpwalidujLogin(), wyczyscTytul(), skrocOpis()
includes/funkcje_terminow.phpkomunikatTerminu(), aktualizujStreak()
includes/funkcje_zadan.phppobierzWykonaneZadania(), pobierzAktywneZadania(), sumujXp(), sortujPoTerminie()
data/zadania.phptablica $zadania
includes/header.phppoczątek strony HTML (od <!DOCTYPE> do <h1>TaskQuest</h1>)
includes/footer.phpkoniec strony HTML
index.phpdane użytkownika i grywalizacji, wywołania funkcji, zawartość panelu

Przygotowanie (zrób to teraz)

  1. Utwórz katalog taskquest/ tam, gdzie trzymasz projekty (np. w katalogu serwera lokalnego).
  2. Skopiuj do niego swój plik TaskQuest z Lekcji 9 i nazwij go index.php.
  3. Zrób kopię zapasową pliku z Lekcji 9 — np. taskquest_lekcja9.php — poza katalogiem taskquest/. Będzie potrzebna do testu regresji (sekcja 8).
  4. Uruchom index.php tak jak w poprzednich lekcjach i zapamiętaj (lub zrób zrzut), jak wygląda strona.

2. Problem w aplikacji

Kama chce zmienić liczbę XP potrzebną na poziom. W jednym długim pliku szuka define('XP_NA_POZIOM', 100) między funkcjami, danymi i kodem HTML. Za kilka lekcji taki plik będzie miał tysiąc linii.

Jest też drugi problem: strona diagnostyczna, którą dołożymy na końcu, potrzebuje tego samego nagłówka HTML co strona główna. Kopiowanie nagłówka do każdej strony oznacza, że każdą zmianę trzeba wprowadzać w kilku miejscach — i któregoś dnia zapomnimy o jednym.

Potrzebujemy:

  1. sposobu, żeby podzielić kod na małe pliki o jasnym przeznaczeniu,
  2. sposobu, żeby PHP skleiło je w całość w odpowiedniej kolejności.

3. Nowy mechanizm PHP: dołączanie plików

3.1 Minimalny przykład

W katalogu taskquest/ utwórz dwa pliki brudnopisu.

pozdrowienie.php

<?php

$powitanie = "Witaj w TaskQuest!";

function krzyknij(string $tekst): string
{
    return strtoupper($tekst) . "!";
}

brudnopis.php

<?php

require_once __DIR__ . '/pozdrowienie.php';

echo $powitanie . "<br>";
echo krzyknij("start") . "<br>";

Otwórz brudnopis.php. Zobaczysz:

Witaj w TaskQuest!
START!

Co się stało? require_once wstawiło zawartość pozdrowienie.php w miejsce, w którym stoi. Dlatego zmienna $powitanie i funkcja krzyknij() są dostępne, jakby były napisane w brudnopis.php. Tak samo będzie ze stałymi i funkcjami TaskQuest.

3.2 Cztery instrukcje

InstrukcjaGdy pliku brakujeDołączenie tego samego pliku drugi raz
includeostrzeżenie (Warning), skrypt działa dalejdozwolone
include_onceostrzeżenie, skrypt działa dalejplik dołączony tylko raz
requirebłąd krytyczny, skrypt zatrzymuje siędozwolone
require_oncebłąd krytyczny, skrypt zatrzymuje sięplik dołączony tylko raz

Zasada kciuka:

  • plik, bez którego aplikacja nie ma sensu (konfiguracja, funkcje, nagłówek strony) → require_once,
  • plik funkcji zawsze z _once — druga próba zadeklarowania tej samej funkcji kończy się błędem krytycznym,
  • include tylko dla rzeczy naprawdę opcjonalnych, których brak nie psuje strony.

To instrukcje, a nie zwykłe funkcje — nawiasy są zbędne, dlatego w TaskQuest piszemy require_once __DIR__ . '/plik.php';.

3.3 Ścieżki: względna czy __DIR__?

Ścieżka względna ('includes/header.php', '../config/config.php') jest liczona od katalogu, z którego uruchomiono skrypt — a nie od pliku, w którym ją wpisałaś/wpisałeś. (PHP próbuje przy tym jeszcze kilku dodatkowych miejsc, więc zachowanie bywa zaskakujące — lepiej się na nim nie opierać.) To częste źródło błędów: ten sam kod działa w jednym pliku, a w innym nie.

__DIR__ to stała PHP, która zawsze zawiera pełną ścieżkę do katalogu pliku, w którym jest zapisana. Sklejona z resztą ścieżki daje adres, który działa zawsze:

echo __DIR__;   // np. /home/kama/taskquest   (dla brudnopis.php)

Dopisz tę linię do brudnopis.php i zobacz, co wypisze. Zapamiętaj wynik — przyda się w eksperymencie.

Dobra praktyka: w plikach zawierających wyłącznie PHP (funkcje, konfiguracja, dane) nie zamykamy tagu ?>. Każdy znak (nawet enter) po ?> trafiłby do przeglądarki jako tekst, a przy cookies i sesjach (Lekcja 14) powodowałoby to trudne do znalezienia błędy. W plikach header.php i footer.php, które są w dużej części HTML-em, wychodzimy z PHP normalnie.

4. SAMODZIELNIE — bonus XP w brudnopisie

Utwórz plik bonus.php, w którym:

  • zdefiniujesz stałą BONUS_XP o wartości 10,
  • napiszesz funkcję dodajBonus(int $xp): int, która zwraca $xp powiększone o BONUS_XP.

W brudnopis.php dołącz bonus.php i wypisz wynik dodajBonus(90). Ma wyjść 100.

HINT

Plik z funkcją i stałą wygląda jak każdy inny plik PHP — zaczyna się od <?php. Dołączasz go tak samo jak pozdrowienie.php.

POMOC
<?php

define('...', ...);

function dodajBonus(int $xp): int
{
    // zwróć $xp powiększone o stałą
}
ROZWIĄZANIE

bonus.php

<?php

define('BONUS_XP', 10);

function dodajBonus(int $xp): int
{
    return $xp + BONUS_XP;
}

brudnopis.php (dopisz pod istniejącym kodem)

require_once __DIR__ . '/bonus.php';

echo dodajBonus(90);

5. SAMODZIELNIE — include czy require?

Bez pisania kodu. Dla każdego pliku TaskQuest wybierz instrukcję (include, include_once, require, require_once) i uzasadnij wybór jednym zdaniem:

PlikTwój wybórUzasadnienie
config/config.php  
includes/funkcje_zadan.php  
includes/header.php  
includes/ogloszenie.php — hipotetyczny plik z „ogłoszeniem dnia”, który czasem istnieje, a czasem nie  

ROZWIĄZANIE

PlikWybórUzasadnienie
config/config.phprequire_oncebez stałych TaskQuest nie policzy poziomów; wystarczy jedno dołączenie
includes/funkcje_zadan.phprequire_oncebez funkcji aplikacja nie działa, a ponowne dołączenie = błąd „Cannot redeclare”
includes/header.phprequire_once (require też jest poprawne)bez nagłówka strona nie ma struktury HTML
includes/ogloszenie.phpincludebrak pliku nie może zepsuć całej strony

6. Dzielimy TaskQuest — krok po kroku

Zasada pracy: po każdym kroku odśwież stronę. Jeżeli coś przestanie działać, wiesz, że błąd jest w ostatnim kroku. Nie przenoś wszystkiego naraz.

Krok 1 — katalogi

W katalogu taskquest/ utwórz podkatalogi config, includes i data.

Krok 2 — konfiguracja

SAMODZIELNIE

Utwórz config/config.php i przenieś do niego stałą XP_NA_POZIOM z index.php. W index.php w tym miejscu dołącz nowy plik.

HINT

Linię z define(...) wytnij z index.php (nie kopiuj — inaczej stała będzie zdefiniowana dwa razy). Do index.php wpisz w jej miejsce require_once.

ROZWIĄZANIE

config/config.php

<?php

// Ustawienia aplikacji TaskQuest
define('XP_NA_POZIOM', 100);

index.php (na samej górze, tam gdzie była stała)

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

Odśwież stronę — wygląda tak samo.

Krok 3 — funkcje grywalizacji

SAMODZIELNIE

Przenieś do includes/funkcje_grywalizacji.php pięć funkcji: obliczPoziom(), pobierzTytul(), obliczXpWAktualnymPoziomie(), obliczProcentPostepu(), ileBrakujeXp(). Same funkcje — wywołania ($poziom = obliczPoziom($xp); itd.) zostają w index.php.

HINT

Nowy plik zaczyna się od <?php. Funkcje przenosisz bez zmian — wycinasz z index.php, wklejasz do nowego pliku. W index.php dołącz plik pod config.php.

POMOC
<?php

// Funkcje systemu XP, poziomów i tytułów

function obliczPoziom(int $xp) : int {
    // ... bez zmian
}

// ... pozostałe cztery funkcje
ROZWIĄZANIE

Kompletny plik znajdziesz w sekcji 10. W index.php dopisz pod config.php:

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

Uwaga na kolejność: wywołania typu $poziom = obliczPoziom($xp); muszą stać pod wszystkimi require_once. Inaczej PHP nie będzie znało funkcji ani stałej w chwili wywołania.

Krok 4 — pozostałe funkcje

SAMODZIELNIE

Tak samo jak w kroku 3 przenieś resztę funkcji, po jednym pliku naraz (po każdym odśwież stronę):

Nowy plikFunkcje
includes/funkcje_tekstowe.phpwalidujLogin(), wyczyscTytul(), skrocOpis()
includes/funkcje_terminow.phpkomunikatTerminu(), aktualizujStreak()
includes/funkcje_zadan.phppobierzWykonaneZadania(), pobierzAktywneZadania(), sumujXp(), sortujPoTerminie()

W index.php dołącz każdy z nich przez require_once __DIR__ . '/includes/...';.

Po tym kroku w index.php nie ma już żadnej definicji funkcji (słowa function). Sprawdź to wyszukiwaniem w edytorze.

Krok 5 — dane zadań

SAMODZIELNIE

Przenieś tablicę $zadania do data/zadania.php i dołącz ten plik w index.php.

HINT

Przenosisz cały zapis $zadania = [ ... ];. Tablica ma się nadal nazywać $zadania — kod niżej (pobierzWykonaneZadania($zadania) itd.) nie może się zmienić.

ROZWIĄZANIE

Plik data/zadania.php — sekcja 10. W index.php:

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

Dlaczego $zadania jest widoczna w index.php? Bo require_once wstawia kod pliku dokładnie w to miejsce — zmienna powstaje tak, jakby była napisana w index.php.

Krok 6 — nagłówek i stopka HTML

SAMODZIELNIE

Utwórz includes/header.php i includes/footer.php.

  • header.php: wszystko od <!DOCTYPE html> do <h1>TaskQuest</h1> (razem z <style> i otwarciem <div class="panel">),
  • footer.php: zamknięcie <div class="panel">, </body> i </html>.

Tytuł strony w <title> ma pochodzić ze zmiennej $nazwaStrony, którą ustawi strona dołączająca nagłówek.

Wyświetlanie panelu (od zdania „Zdobywaj punkty…” do końca listy zadań) zostaje w index.php.

HINT
  • header.php to zwykły HTML z jedną wstawką PHP w <title>. Do wypisania zmiennej użyj skróconego <?= ... ; ?>, które już znasz.
  • Zmienną $nazwaStrony trzeba ustawić przed dołączeniem header.php. Pomyśl: skąd nagłówek miałby ją znać, gdyby jeszcze nie istniała?

Uwaga na nazwy: to trzecia rzecz związana z „tytułem” w TaskQuest — obok $tytul (tytuł gracza) i $zadanie['tytul'] (tytuł zadania). Dlatego nazwa strony to $nazwaStrony, a nie $tytul....

POMOC

includes/header.php (początek)

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $nazwaStrony; ?></title>
    <style>
        /* style bez zmian */
    </style>
</head>
<body>
    <div class="panel">
        <h1>TaskQuest</h1>

index.php — na końcu części PHP, a potem HTML panelu:

$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?>
        <p>Zdobywaj punkty za wykonane zadania!</p>
        <!-- reszta panelu bez zmian -->
<?php
require_once __DIR__ . '/includes/footer.php';
ROZWIĄZANIE

Kompletne header.php i footer.php — sekcja 10. Do stopki dodaliśmy linię TaskQuest 1.0 (jedyna różnica na stronie względem Lekcji 9).

Krok 7 — porządek w index.php

Sprawdź, czy index.php ma kolejność:

1. require_once (config, funkcje, dane)
2. dane użytkownika        ($imieUzytkownika, $login)
3. dane grywalizacji       ($xp, $streak, $dataOstatniejAktywnosci)
4. wywołania funkcji       ($poziom, $tytul, ..., $zadanieWykonane, $streak)
5. $nazwaStrony + header
6. zawartość panelu
7. footer

W index.php zostają dane użytkownika i grywalizacji — to na razie dane symulowane, które w kolejnych lekcjach zastąpią prawdziwe źródła. Wywołania funkcji zostają dokładnie takie, jakie były.

7. Kolejne zadanie — strona diagnostyczna

Problem

Plików jest już prawie dziesięć. Jedna literówka w nazwie (funkcje_zadan.php vs funkcje_zadania.php) i aplikacja wyświetla błąd krytyczny, a Ty musisz zgadywać, którego pliku brakuje. Potrzebujemy narzędzia dla programisty, które sprawdzi strukturę projektu.

Nowy mechanizm: funkcje plików i katalogów

Przećwicz w brudnopis.php (dopisz na końcu). Przed uruchomieniem przewidź wynik każdej linii:

$sciezka = __DIR__ . '/data/zadania.php';

echo basename($sciezka) . "<br>";                     // nazwa pliku
echo dirname($sciezka) . "<br>";                      // katalog, w którym leży
echo pathinfo($sciezka, PATHINFO_EXTENSION) . "<br>"; // rozszerzenie

var_dump(is_file(__DIR__ . '/data/zadania.php'));     // czy to plik?
var_dump(is_dir(__DIR__ . '/data'));                  // czy to katalog?
var_dump(is_file(__DIR__ . '/data'));                 // ??? (katalog to nie plik)

echo '<pre>';
print_r(scandir(__DIR__ . '/data'));                  // zawartość katalogu
echo '</pre>';

Ważne cechy scandir():

  • zwraca tablicę nazw (posortowaną alfabetycznie),
  • zawsze zawiera dwa „udawane” wpisy: . (ten katalog) i .. (katalog nadrzędny) — trzeba je pominąć,
  • dla katalogu, który nie istnieje, zwraca false i wypisuje ostrzeżenie.

Dodatkowo mkdir() tworzy katalog, a rmdir() usuwa pusty katalog. Sprawdź w brudnopisie:

$katalog = __DIR__ . '/tymczasowy';

if (!is_dir($katalog)) {
    mkdir($katalog);
}

var_dump(is_dir($katalog));   // true
rmdir($katalog);              // sprzątamy po sobie
var_dump(is_dir($katalog));   // false

Zwróć uwagę na wzorzec: najpierw sprawdź (is_dir()), potem działaj (mkdir()).

SAMODZIELNIE — czyPlikIstnieje()

Utwórz plik includes/funkcje_diagnostyczne.php i napisz w nim funkcję:

function czyPlikIstnieje(string $sciezkaWzgledna): bool

Ma zwracać true, jeśli w katalogu projektu (taskquest/) istnieje plik o podanej ścieżce, np. 'config/config.php'.

HINT

Plik leży w includes/, a katalog projektu jest o jeden poziom wyżej. Jedna funkcja zwraca katalog nadrzędny wskazanej ścieżki — użyj jej na __DIR__.

POMOC
function czyPlikIstnieje(string $sciezkaWzgledna): bool
{
    // dirname(__DIR__) to katalog projektu
    // sklej go z '/' i $sciezkaWzgledna, sprawdź is_file()
}
ROZWIĄZANIE
function czyPlikIstnieje(string $sciezkaWzgledna): bool
{
    return is_file(dirname(__DIR__) . '/' . $sciezkaWzgledna);
}

SAMODZIELNIE — listaPlikowWKatalogu()

W tym samym pliku napisz funkcję:

function listaPlikowWKatalogu(string $katalog, string $rozszerzenie = 'php'): array

Zwraca tablicę nazw plików z podanego katalogu projektu, ale:

  • pomija . i ..,
  • bierze tylko pliki (nie katalogi) o podanym rozszerzeniu (domyślnie php),
  • dla katalogu, który nie istnieje, zwraca pustą tablicę bez żadnego ostrzeżenia.

(Rozszerzenie jako parametr przyda się w Lekcji 11, gdy w data/ pojawią się pliki innego typu.)

HINT
  • Zacznij od zabezpieczenia: is_dir() przed scandir().
  • . i .. pomijasz przez continue — znasz je z pętli.
  • Rozszerzenie odczytasz przez pathinfo(..., PATHINFO_EXTENSION).
POMOC
function listaPlikowWKatalogu(string $katalog, string $rozszerzenie = 'php'): array
{
    $sciezka = dirname(__DIR__) . '/' . $katalog;
    $pliki = [];

    // 1. jeśli to nie katalog → zwróć $pliki (pustą tablicę)

    foreach (scandir($sciezka) as $element) {
        // 2. pomiń . i ..
        // 3. jeśli to plik z właściwym rozszerzeniem → dopisz do $pliki
    }

    return $pliki;
}
ROZWIĄZANIE
function listaPlikowWKatalogu(string $katalog, string $rozszerzenie = 'php'): array
{
    $sciezka = dirname(__DIR__) . '/' . $katalog;
    $pliki = [];

    if (!is_dir($sciezka)) {
        return $pliki;
    }

    foreach (scandir($sciezka) as $element) {
        if ($element === '.' || $element === '..') {
            continue;
        }

        if (is_file($sciezka . '/' . $element) && pathinfo($element, PATHINFO_EXTENSION) === $rozszerzenie) {
            $pliki[] = $element;
        }
    }

    return $pliki;
}

SAMODZIELNIE — strona diagnostyka.php

W katalogu głównym (taskquest/) utwórz diagnostyka.php. Strona ma:

  1. korzystać z tych samych header.php i footer.php co strona główna (to jest właśnie zysk z Kroku 6!),
  2. dla każdego pliku z tablicy $wymaganePliki (10 plików z docelowej struktury) wypisać [OK] lub [BRAK] i jego ścieżkę,
  3. dla katalogów config, includes i data wypisać liczbę plików .php i ich nazwy.

Ważne: diagnostyka.php dołącza tylko funkcje_diagnostyczne.php (oraz header i footer). Nie dołącza config.php ani funkcji aplikacji. Dlaczego? Bo ma działać nawet wtedy, gdy reszta projektu jest zepsuta — inaczej nie mogłaby jej diagnozować.

HINT

Dwie pętle foreach: jedna po $wymaganePliki, druga po $katalogiDoPrzegladu = ['config', 'includes', 'data']. Znacznik [OK]/[BRAK] zrobisz operatorem ? : — jak [X]/[ ] w liście zadań.

POMOC
<?php

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

$wymaganePliki = [
    'index.php',
    'config/config.php',
    // ... pozostałe
];

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

$nazwaStrony = 'TaskQuest — diagnostyka';
require_once __DIR__ . '/includes/header.php';
?>
        <h2>Diagnostyka projektu</h2>
        <!-- pętle foreach -->
<?php
require_once __DIR__ . '/includes/footer.php';
ROZWIĄZANIE

Sekcja 10.

Uwaga: to narzędzie dla programisty, a nie dla użytkownika aplikacji. Pokazywanie budowy projektu wszystkim to zły pomysł — do zagadnienia tego, co wolno pokazywać użytkownikowi, wrócimy w lekcji o bezpieczeństwie.

8. Testy

Przed uruchomieniem spróbuj przewidzieć wynik.

Test 1 — struktura (strona diagnostyka.php)

Co sprawdzamyOczekiwany wynik
lista wymaganych plikówwszystkie 10 z [OK]
config/1 plik: config.php
includes/7 plików
data/1 plik: zadania.php

Test 2 — regresja: strona główna działa jak po Lekcji 9

Otwórz obok siebie stronę z kopii taskquest_lekcja9.php i nową index.php. Jedyna różnica: linia TaskQuest 1.0 na dole.

Wartości niezależne od daty (przy $xp = 99):

Co sprawdzamyOczekiwany wynik
Poziom / Tytuł1 / Nowicjusz
Nagłówek postępuMasz 99 XP z 100 XP potrzebnych do poziomu 2 (99%)
Brakuje XP1
Wszystkie / Wykonane / Aktywne5 / 2 / 3
XP do zdobycia z aktywnych55
Komunikat awansuawansujesz na poziom 2
Kolejność id na liście2, 3, 1, 5, 4

Komunikaty terminów (minął ... dni temu, pozostało ... dni) i wartość streaka zależą od dnia uruchomienia — porównaj je z kopią z Lekcji 9 uruchomioną tego samego dnia.

Test 3 — jedna zmiana w config.php

Zmień w config/config.php wartość XP_NA_POZIOM (nic więcej nie ruszaj!):

XP_NA_POZIOMPoziomTytułNagłówek postępuKolejny prógBrakuje XPKomunikat
1001Nowicjusz99 XP z 100 … do poziomu 2 (99%)1001awansujesz na poziom 2
2001Nowicjusz99 XP z 200 … do poziomu 2 (49.5%)200101nie wystarczą — brakuje 46 XP
502Wojownik49 XP z 50 … do poziomu 3 (98%)1001awansujesz na poziom 3

Przywróć 100.

Test 4 — funkcje diagnostyczne

WywołanieOczekiwany wynik
czyPlikIstnieje('index.php')true
czyPlikIstnieje('config/config.php')true
czyPlikIstnieje('config')false (to katalog, nie plik)
czyPlikIstnieje('nie_ma.php')false
listaPlikowWKatalogu('data')['zadania.php']
listaPlikowWKatalogu('data', 'csv')[]
listaPlikowWKatalogu('nie_ma')[], bez ostrzeżenia
listaPlikowWKatalogu('includes')footer.php, funkcje_diagnostyczne.php, funkcje_grywalizacji.php, funkcje_tekstowe.php, funkcje_terminow.php, funkcje_zadan.php, header.php (alfabetycznie)

Test 5 — liczby z Lekcji 9 nadal się zgadzają

Co sprawdzamyWynik
count($zadania)5
$liczbaWykonanych / $liczbaAktywnych2 / 3
sumujXp($zadania) / sumujXp($aktywneZadania) / sumujXp($wykonaneZadania)100 / 55 / 45
sumujXp([])0

Test 6 — diagnostyka wykrywa problem

Zmień nazwę includes/funkcje_zadan.php na includes/funkcje_zadania.php.

  • index.php przestaje działać (błąd krytyczny),
  • diagnostyka.php nadal działa i pokazuje [BRAK] includes/funkcje_zadan.php.

Przywróć poprawną nazwę.

9. Obowiązkowe eksperymenty

Po każdym eksperymencie przywróć projekt do stanu działającego.

Eksperyment 1: dwa razy ten sam plik

W index.php zamień require_once dla funkcje_zadan.php na zwykły require i wpisz tę linię dwa razy. Co zobaczysz? Dlaczego? Co robił _once?

Oczekiwany efekt: błąd krytyczny „Cannot redeclare pobierzWykonaneZadania()”.

Eksperyment 2: brakujący plik — include czy require_once?

Zmień nazwę includes/funkcje_zadan.php na includes/x.php.

a) W index.php zastąp dla tego pliku require_once przez include. Odśwież. Ile komunikatów zobaczysz? W którym miejscu kończy się działanie skryptu i jaki błąd jest widoczny?

b) Wróć do require_once. Odśwież. Porównaj komunikat.

Odpowiedz: który wariant szybciej i wyraźniej wskazuje prawdziwą przyczynę problemu?

Oczekiwany efekt: przy include najpierw ostrzeżenie o braku pliku, a dopiero później (w linii, gdzie wywołujesz funkcję) błąd „Call to undefined function”. Przy require_once skrypt zatrzymuje się od razu.

Eksperyment 3: ../ a __DIR__

Na samą górę includes/header.php dopisz:

<?php require_once '../config/config.php'; ?>

Odśwież stronę główną. Dlaczego PHP nie znajduje pliku, skoro config/ jest „obok” katalogu includes/? Wskazówka: przypomnij sobie sekcję 3.3 — od czego liczona jest ścieżka względna?

Teraz zamień linię na:

<?php require_once __DIR__ . '/../config/config.php'; ?>

Działa? Czy stała XP_NA_POZIOM została zdefiniowana drugi raz? (Nie — require_once rozpoznaje, że to ten sam plik.)

Eksperyment 4 (dodatkowy): zmienna zapomniana przed nagłówkiem

Usuń z index.php linię $nazwaStrony = 'TaskQuest';. Jaki komunikat zobaczysz i w którym pliku? Co to mówi o tym, jak dołączone pliki „widzą” zmienne?

Eksperyment 5 (dodatkowy): scandir() bez is_dir()

W listaPlikowWKatalogu() zakomentuj sprawdzenie is_dir() i wywołaj funkcję dla 'nie_ma'. Ile ostrzeżeń zobaczysz i skąd drugie? (Wskazówka: co zwróciło scandir() i co próbowała zrobić pętla foreach?)

10. ROZWIĄZANIE — wersja referencyjna

Pełny kod TaskQuest po Lekcji 10. Wszystkie funkcje i zmienne pochodzą z Lekcji 9 — zmienił się tylko podział na pliki. Nowe elementy: header.php, footer.php, $nazwaStrony, funkcje_diagnostyczne.php, diagnostyka.php.

Rozwiązanie

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__ . '/data/zadania.php';


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


// --- 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';

config/config.php

<?php

// Ustawienia aplikacji TaskQuest
define('XP_NA_POZIOM', 100);

includes/funkcje_grywalizacji.php

<?php

// Funkcje systemu XP, poziomów i tytułów

function obliczPoziom(int $xp) : int {
    $poziom = 1;

    while ($xp >= $poziom * XP_NA_POZIOM) {
        $poziom++;
    }

    return $poziom;
}


function pobierzTytul(int $poziom) : string {
    return match ($poziom) {
        1 => "Nowicjusz",
        2 => "Wojownik",
        default => "Mistrz"
    };
}


function obliczXpWAktualnymPoziomie(int $xp, int $poziom) : int {
    $xpNaPoczatkuPoziomu = ($poziom - 1) * XP_NA_POZIOM;
    $xpWAktualnymPoziomie = $xp - $xpNaPoczatkuPoziomu;

    return $xpWAktualnymPoziomie;
}


function obliczProcentPostepu(int $xpWAktualnymPoziomie) : float {
    return ($xpWAktualnymPoziomie / XP_NA_POZIOM) * 100;
}


function ileBrakujeXp(int $xp, int $poziom): int
{
    $progNastepnegoPoziomu = $poziom * XP_NA_POZIOM;

    return $progNastepnegoPoziomu - $xp;
}

includes/funkcje_tekstowe.php

<?php

// Funkcje przygotowujące i sprawdzające tekst (login, tytuł, opis)

function walidujLogin(string $login): bool {
    return preg_match('/^[a-zA-Z0-9_-]{3,20}$/', $login) === 1;
}


function wyczyscTytul(string $tytul): string
{
    $tytul = trim(preg_replace('/\s+/', ' ', $tytul));

    return $tytul;
}


function skrocOpis(string $opis, int $limit = 50): string {
    if (mb_strlen($opis) <= $limit) {
        return $opis;
    }

    return mb_substr($opis, 0, $limit) . "...";
}

includes/funkcje_terminow.php

<?php

// Funkcje związane z datami: terminy zadań i streak

function komunikatTerminu(string $terminZadania): string{
    $dzisiaj = new DateTime('today');
    $termin = new DateTime($terminZadania);
    $roznica = $dzisiaj->diff($termin);

    if ($termin < $dzisiaj) {
        return "Termin minął {$roznica->days} dni temu!";
    } elseif ($termin == $dzisiaj) {
        return "Termin jest dzisiaj.";
    } else{
        return "Do terminu pozostało: {$roznica->days} dni.";
    }
}


function aktualizujStreak(int $streak, string $dataOstatniejAktywnosci, bool $zadanieWykonane): int {
    if (!$zadanieWykonane) return $streak;

    $dzisiaj = new DateTime('today');
    $wczoraj = new DateTime('yesterday');
    $ostatniaAktywnosc = new DateTime($dataOstatniejAktywnosci);

    if ($ostatniaAktywnosc->format('Y-m-d') === $dzisiaj->format('Y-m-d')) return $streak;

    if ($ostatniaAktywnosc->format('Y-m-d') === $wczoraj->format('Y-m-d')) return $streak + 1;

    return 1;
}

includes/funkcje_zadan.php

<?php

// Funkcje pracujące na liście zadań

function pobierzWykonaneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return $zadanie['wykonane'];
    });
}


function pobierzAktywneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return !$zadanie['wykonane'];
    });
}


function sumujXp(array $zadania): int
{
    $suma = 0;

    foreach ($zadania as $zadanie) {
        $suma += $zadanie['xp'];
    }

    return $suma;
}


function sortujPoTerminie(array $zadania): array
{
    usort($zadania, function ($zadanieA, $zadanieB) {
        return $zadanieA['termin'] <=> $zadanieB['termin'];
    });

    return $zadania;
}

data/zadania.php

<?php

// Lista zadań TaskQuest — na razie zapisana "na sztywno" (dane symulowane).
// W Lekcji 11 zastąpimy ją prawdziwym plikiem z danymi.

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

includes/header.php

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $nazwaStrony; ?></title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 700px; margin: 40px auto; padding: 20px; }
        .panel { border: 1px solid #ccc; padding: 24px; border-radius: 12px; }
        .pasek-xp { display: flex; gap: 4px; margin: 10px 0; }
        .segment { width: 32px; height: 18px; background: #ddd; border-radius: 3px; }
        .segment.aktywny { background: #555; }
        .awans { font-weight: bold; }
        .zadanie { border: 1px solid #ddd; border-radius: 8px; padding: 4px 16px; margin: 10px 0; }
        .zadanie.wykonane { background: #f2f2f2; color: #666; }
    </style>
</head>
<body>
    <div class="panel">
        <h1>TaskQuest</h1>

includes/footer.php

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

includes/funkcje_diagnostyczne.php

<?php

// Funkcje pomocnicze dla strony diagnostycznej (narzędzie dla programisty)

function czyPlikIstnieje(string $sciezkaWzgledna): bool
{
    return is_file(dirname(__DIR__) . '/' . $sciezkaWzgledna);
}


function listaPlikowWKatalogu(string $katalog, string $rozszerzenie = 'php'): array
{
    $sciezka = dirname(__DIR__) . '/' . $katalog;
    $pliki = [];

    if (!is_dir($sciezka)) {
        return $pliki;
    }

    foreach (scandir($sciezka) as $element) {
        if ($element === '.' || $element === '..') {
            continue;
        }

        if (is_file($sciezka . '/' . $element) && pathinfo($element, PATHINFO_EXTENSION) === $rozszerzenie) {
            $pliki[] = $element;
        }
    }

    return $pliki;
}

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',
    'data/zadania.php'
];

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

$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>";
            }
        }
        ?>
<?php
require_once __DIR__ . '/includes/footer.php';

Pliki brudnopisu (brudnopis.php, pozdrowienie.php, bonus.php) możesz usunąć.

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
define('XP_NA_POZIOM', 100)

wszystkie 14 funkcji z poprzednich lekcji (kod bez zmian)

Dochodzi

$nazwaStrony                               // tytuł strony dla header.php
$wymaganePliki, $katalogiDoPrzegladu       // diagnostyka.php

czyPlikIstnieje(), listaPlikowWKatalogu()  // includes/funkcje_diagnostyczne.php

nowe pliki: config/config.php, includes/*.php (7), data/zadania.php, diagnostyka.php

Nowe narzędzia PHP: require_once, include, require, include_once, __DIR__, dirname(), is_file(), is_dir(), scandir(), pathinfo(), basename(), mkdir(), rmdir().

Zmieniamy

  • miejsce w kodzie: stała, funkcje, dane zadań i szkielet HTML przeniesione z index.php do osobnych plików (logika i wygląd strony głównej bez zmian),
  • index.php zaczyna się od bloku require_once i kończy dołączeniem stopki.

Usuwamy

Nic. Zmiany dotyczą wyłącznie podziału na pliki — żadna zmienna ani funkcja nie została usunięta.

Co nowego potrafi TaskQuest po tej lekcji?

Po Lekcji 10 TaskQuest jest aplikacją wieloplikową: konfiguracja, funkcje, dane i szkielet strony leżą w osobnych plikach, a jedna zmiana (np. progu XP) dotyczy jednego miejsca. Strona diagnostyka.php sprawdza, czy wszystkie wymagane pliki istnieją, i wyświetla zawartość katalogów projektu.

Do samodzielnego sprawdzenia

[ ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
[ ] Mam katalogi config/, includes/ i data/.
[ ] W index.php nie ma żadnej definicji funkcji ani stałej XP_NA_POZIOM.
[ ] Wszystkie pliki z funkcjami i konfiguracją dołączam przez require_once i __DIR__.
[ ] Potrafię wyjaśnić różnicę między include a require oraz po co jest _once.
[ ] Wiem, dlaczego ścieżka z __DIR__ jest pewniejsza niż względna.
[ ] Strona główna wygląda i działa jak po Lekcji 9 (jedyna różnica: linia "TaskQuest 1.0").
[ ] Zmiana XP_NA_POZIOM w jednym pliku zmienia zachowanie całej aplikacji.
[ ] Mam header.php i footer.php, a $nazwaStrony ustawiam przed dołączeniem nagłówka.
[ ] Napisałam/napisałem czyPlikIstnieje() i listaPlikowWKatalogu().
[ ] Strona diagnostyka.php pokazuje 10 razy [OK].
[ ] Przeprowadziłam/przeprowadziłem eksperymenty i przywróciłam/przywróciłem projekt do stanu działającego.
[ ] Usunęłam/usunąłem pliki brudnopisu.

Pytania kontrolne

  1. Podaj dwa konkretne powody, dla których podzieliliśmy TaskQuest na pliki.
  2. Co się stanie po include 'nie_ma.php';, a co po require 'nie_ma.php';? Który wariant jest groźniejszy w pliku z funkcjami — i dlaczego?
  3. Dlaczego pliki z funkcjami dołączamy przez require_once, a nie require? Co zobaczysz, jeśli dołączysz taki plik dwa razy?
  4. Co przechowuje __DIR__ w pliku index.php, a co w includes/header.php?
  5. Dlaczego require_once '../config/config.php'; w header.php nie działa, a require_once __DIR__ . '/../config/config.php'; działa?
  6. Skąd header.php zna zmienną $nazwaStrony? Co się stanie, jeśli ustawisz ją po dołączeniu nagłówka?
  7. Co zwraca scandir() oprócz nazw plików? Jak sobie z tym radzimy i co funkcja zwróci dla nieistniejącego katalogu?
  8. czyPlikIstnieje('config') zwróci true czy false? Dlaczego?
  9. Dlaczego diagnostyka.php nie dołącza config.php ani funkcji aplikacji?
  10. Zmieniasz XP_NA_POZIOM w config/config.php z 100 na 50. Które pliki musisz jeszcze zmienić, żeby cała aplikacja liczyła poziomy po nowemu? Co to mówi o zaletach podziału na pliki?

Zadanie końcowe — rozbuduj TaskQuest samodzielnie

Wybierz jedno (albo oba):

A) Style w osobnym pliku. Utwórz assets/css/style.css, przenieś do niego zawartość <style> z header.php i podłącz plik tagiem <link rel="stylesheet" href="assets/css/style.css">. Dodaj 'assets/css/style.css' do listy $wymaganePliki w diagnostyce. Zauważ: href w HTML nie używa __DIR__ — to adres, który pobiera przeglądarka, a nie ścieżka, którą czyta PHP. Pomyśl, dlaczego.

B) Katalog logs/. Dodaj do diagnostyki sprawdzanie, czy istnieje katalog logs/. Jeśli nie — utwórz go przez mkdir() (najpierw is_dir()!) i wyświetl komunikat „Utworzono katalog logs/”, a przy kolejnym odświeżeniu „Katalog logs/ istnieje”.

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

Most do Lekcji 11

Kod TaskQuest jest już uporządkowany, ale zadania nadal są częścią programu:

data/zadania.php   →   $zadania = [ ... ];   (kod PHP)

Żeby dodać albo zmienić zadanie, musisz edytować kod. Każde zadanie napisane w formularzu (Lekcja 13) musiałoby zmieniać kod aplikacji — a to nie jest dobry pomysł.

Następny problem:

Jak oddzielić dane od programu — czyli przechowywać zadania w prawdziwym pliku, który TaskQuest potrafi odczytać i zapisać?


PODSUMOWANIE LEKCJI

W skrócie

  • include / require wstawiają kod pliku w miejsce, w którym stoją. Zmienne, funkcje i stałe z dołączonego pliku istnieją dopiero od tej linii w dół.
  • Kolejność ma znaczenie: najpierw require_once, potem wywołania funkcji. Zmienną używaną w dołączanym pliku (np. $nazwaStrony w header.php) ustawiamy przed jego dołączeniem.
  • Brak pliku: include → ostrzeżenie i skrypt działa dalej (błąd wychodzi później, w innym miejscu), require → skrypt zatrzymuje się od razu.
  • _once pilnuje, żeby plik był dołączony tylko raz. Funkcji nie da się zadeklarować dwa razy → błąd krytyczny Cannot redeclare. Działa to tylko między instrukcjami _once — zwykły require po require_once dołączy plik ponownie.
  • Ścieżka względna w include/require nie jest liczona od pliku, w którym ją zapisano. __DIR__ = katalog pliku, w którym stała jest zapisana — dlatego ścieżka __DIR__ . '/...' działa zawsze.
  • dirname(__DIR__) = katalog o poziom wyżej (w TaskQuest: z includes/ do katalogu projektu).
  • Po __DIR__ ścieżka musi zaczynać się od /: __DIR__ . '/config/config.php'.
  • __DIR__ to ścieżka na dysku serwera — służy PHP. W href i src w HTML podajemy adres dla przeglądarki (np. styl.css), bez __DIR__.
  • scandir() zwraca tablicę nazw razem z . i ... Dla nieistniejącego katalogu zwraca false i ostrzeżenie → najpierw is_dir().
  • is_file() zwraca true tylko dla pliku, is_dir() tylko dla katalogu.
  • mkdir() na istniejącym katalogu → ostrzeżenie. Zagnieżdżone katalogi tylko z trzecim parametrem true. rmdir() usuwa wyłącznie pusty katalog.
  • W plikach zawierających tylko PHP nie zamykamy tagu ?>.
  • Nigdy nie wstawiamy do include danych od użytkownika bez sprawdzenia (lista dozwolonych wartości).

Co trzeba umieć na INF.03?

  • poprawnie połączyć zewnętrzny plik CSS i obrazy ścieżką względną (href="styl.css", src="obraz.jpg") i odróżniać ją od ścieżki do pliku, którą czyta PHP,
  • rozumieć podział strony na bloki (nagłówek, panel boczny, część główna, stopka) — arkusz jest zbudowany właśnie z takich bloków,
  • rozpoznać, dlaczego skrypt kończy się błędem „Call to undefined function” albo „Failed opening required” i gdzie szukać przyczyny,
  • znać różnicę między include a require — to podstawowa wiedza o PHP, ale w materiałach projektu nie ma potwierdzenia, że jest sprawdzana na egzaminie.

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

  1. Pliki z funkcjami i konfiguracją: require_once __DIR__ . '/sciezka/plik.php';
  2. include przy braku pliku → ostrzeżenie, działa dalej. require → zatrzymuje skrypt.
  3. _once = tylko raz. Dwa razy ta sama funkcja → Cannot redeclare.
  4. Najpierw dołącz, potem używaj. Zmienne dla nagłówka ustaw przed jego dołączeniem.
  5. __DIR__ = katalog bieżącego pliku, dirname(__DIR__) = poziom wyżej.
  6. scandir() zawsze zawiera . i ... Przed scandir() i mkdir() sprawdź is_dir().
  7. rmdir() usuwa tylko pusty katalog.

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

Dołączanie plików

InstrukcjaBrak plikuDrugie dołączenie
includeostrzeżenie, działa dalejtak
include_onceostrzeżenie, działa dalejnie
requirebłąd krytyczny, stoptak
require_oncebłąd krytyczny, stopnie
require_once __DIR__ . '/config/config.php';         // z pliku w katalogu głównym
require_once __DIR__ . '/../config/config.php';      // z pliku w includes/

Ścieżki

ZapisWynik dla /var/www/taskquest/data/zadania.php
basename($s)zadania.php
basename($s, '.php')zadania
dirname($s)/var/www/taskquest/data
pathinfo($s, PATHINFO_EXTENSION)php
pathinfo($s, PATHINFO_FILENAME)zadania
__DIR__katalog pliku, w którym stała jest zapisana
__FILE__pełna ścieżka pliku, w którym stała jest zapisana

Pliki i katalogi

FunkcjaZwraca / robi
is_file($s)true tylko dla pliku
is_dir($s)true tylko dla katalogu
scandir($s)tablica nazw (z . i ..) lub false
mkdir($s)tworzy katalog
mkdir($s, 0755, true)tworzy także brakujące katalogi po drodze
rmdir($s)usuwa pusty katalog
getcwd() / chdir($s)bieżący katalog roboczy / zmiana katalogu

Schematy

// utwórz katalog tylko jeśli go nie ma
if (!is_dir($katalog)) {
    mkdir($katalog);
}

// przejście po katalogu bez . i ..
foreach (scandir($katalog) as $element) {
    if ($element === '.' || $element === '..') {
        continue;
    }
    // ...
}

Szkielet strony z nagłówkiem i stopką

$nazwaStrony = '...';
require_once __DIR__ . '/includes/header.php';
?>
    <!-- treść strony -->
<?php
require_once __DIR__ . '/includes/footer.php';

Quiz – sprawdź się

1. Co zostanie wyświetlone? Pliku brak.php nie ma.

include 'brak.php';
echo "A";
require 'brak.php';
echo "B";

2. Plik a.php zawiera tylko <?php $licznik = 1;. Co wyświetli poniższy kod?

$licznik = 5;
require __DIR__ . '/a.php';
echo $licznik;

3. Plik funkcje.php zawiera definicję funkcji. W której linii pojawi się błąd krytyczny i jaki?

require_once __DIR__ . '/funkcje.php';   // linia 1
require_once __DIR__ . '/funkcje.php';   // linia 2
require __DIR__ . '/funkcje.php';        // linia 3

4. Dlaczego ten kod nie działa? Jak go poprawić?

require_once __DIR__ . '/config/config.php';
$poziom = obliczPoziom(150);
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';

5. Plik ma ścieżkę /var/www/taskquest/includes/funkcje_diagnostyczne.php. Co wypiszą poniższe instrukcje zapisane w tym pliku?

echo __DIR__;
echo dirname(__DIR__);
echo basename(__FILE__, '.php');

6. Co zostanie wyświetlone?

$plik = 'raport.2026.csv';
echo pathinfo($plik, PATHINFO_EXTENSION) . " | " . pathinfo($plik, PATHINFO_FILENAME);

7. Katalog data/ zawiera plik zadania.php i podkatalog archiwum. Ile wyniesie count(scandir(__DIR__ . '/data')) (skrypt leży w katalogu projektu)? Wymień wszystkie elementy.

8. Uzupełnij kod tak, aby katalog logs był tworzony tylko wtedy, gdy jeszcze nie istnieje:

$katalog = __DIR__ . '/logs';

if (________________) {
    ________________;
}

9. Katalog a nie istnieje. Co się stanie po wykonaniu mkdir(__DIR__ . '/a/b/c');? Jak poprawić wywołanie?

10. Plik includes/header.php ma dołączyć config/config.php. Która wersja jest poprawna? Co jest nie tak z pozostałymi?

a) require_once 'config/config.php';
b) require_once '../config/config.php';
c) require_once __DIR__ . '/../config/config.php';
d) require_once __DIR__ . 'config/config.php';

11. Dlaczego rmdir(__DIR__ . '/data'); w katalogu projektu TaskQuest nie usunie katalogu data?

12. Co jest niebezpieczne w tym kodzie? Jak to poprawić?

include 'pages/' . $_GET['strona'] . '.php';

13. Napisz funkcję czyKatalogIstnieje(string $sciezkaWzgledna): bool do pliku includes/funkcje_diagnostyczne.php — analogicznie do czyPlikIstnieje(). Dla 'data' ma zwrócić true, dla 'index.php' — false.

14. Dlaczego w <link rel="stylesheet" href="..."> nie wpisujemy ścieżki z __DIR__? Co dostałaby wtedy przeglądarka?


Odpowiedzi
  1. Dwa ostrzeżenia od include, potem A, potem ostrzeżenie i błąd krytyczny Failed opening required 'brak.php'. B nie zostanie wyświetlone.
  2. 1 — kod z a.php wykonuje się w miejscu require i nadpisuje zmienną.
  3. Linia 3: Cannot redeclare .... Linia 2 jest pomijana dzięki _once, ale zwykły require nie sprawdza, czy plik był już dołączony — próbuje zadeklarować funkcję drugi raz.
  4. Funkcja jest wywoływana, zanim plik z jej definicją został dołączony → Call to undefined function obliczPoziom(). Poprawka: przenieść require_once dla funkcje_grywalizacji.php nad wywołanie.
  5. /var/www/taskquest/includes, /var/www/taskquest, funkcje_diagnostyczne.
  6. csv | raport.2026 — rozszerzeniem jest tylko część po ostatniej kropce.
  7. 4: ., .., archiwum, zadania.php. scandir() zwraca także katalogi, nie tylko pliki.
  8. !is_dir($katalog) oraz mkdir($katalog).
  9. Ostrzeżenie „No such file or directory”, funkcja zwraca false i nic nie tworzy. Poprawnie: mkdir(__DIR__ . '/a/b/c', 0755, true);.
  10. Poprawna jest c. a) i b) to ścieżki względne liczone od katalogu uruchomionego skryptu, a nie od header.php — gdy header.php jest dołączany z index.php, '../config/config.php' wskazuje poza projekt. d) brakuje / po __DIR__, powstaje .../includesconfig/config.php.
  11. rmdir() usuwa tylko pusty katalog, a w data/ jest zadania.php. Funkcja zwróci false i wyświetli ostrzeżenie „Directory not empty”.
  12. Użytkownik może w adresie podać np. ../../config/config i dołączyć dowolny plik (Path Traversal). Poprawka: lista dozwolonych stron i sprawdzenie, np.: $dozwolone = ['home', 'about', 'contact']; $strona = $_GET['strona'] ?? 'home'; if (in_array($strona, $dozwolone)) { include __DIR__ . '/pages/' . $strona . '.php'; }
  13. function czyKatalogIstnieje(string $sciezkaWzgledna): bool { return is_dir(dirname(__DIR__) . '/' . $sciezkaWzgledna); }
  14. href to adres, który pobiera przeglądarka (liczony od adresu strony, np. http://localhost/taskquest/). __DIR__ to ścieżka na dysku serwera (np. /var/www/taskquest albo C:\xampp\htdocs\taskquest) — przeglądarka nie ma do niej dostępu, więc styl by się nie wczytał. Dlatego piszemy np. href="styl.css" lub href="assets/css/style.css".